Compare commits
	
		
			288 Commits 
		
	
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 77f5921dff | |
|  | 29c2ecf40c | |
|  | 656cd3c976 | |
|  | d90320f5f9 | |
|  | 3695761b9e | |
|  | 692fec9bfe | |
|  | 5f9a0fe75b | |
|  | d7b029c255 | |
|  | 77be82d8e6 | |
|  | f79c1765b6 | |
|  | de486ba7e7 | |
|  | eebb815ad1 | |
|  | 49e147c5b5 | |
|  | a77cf5b328 | |
|  | f22fa76987 | |
|  | 08953c6272 | |
|  | 315b68f928 | |
|  | 056388be71 | |
|  | dfcc1dc7d8 | |
|  | d71f3f4f62 | |
|  | 3e72568141 | |
|  | 1993a7e2de | |
|  | 91cabf56e7 | |
|  | aea05ce0e6 | |
|  | 85a489244d | |
|  | 37b65897df | |
|  | 361a1e6c6d | |
|  | 51f0b0b369 | |
|  | 30ea7ff5c0 | |
|  | 55829faf85 | |
|  | 37566b5122 | |
|  | 8203c10f37 | |
|  | b1dae54aac | |
|  | d65b7bcc55 | |
|  | a0f3e84432 | |
|  | c9490cf149 | |
|  | ad8c833862 | |
|  | 47875289a8 | |
|  | e544176cdd | |
|  | 65f9c4da3c | |
|  | bdcc308188 | |
|  | 06f8229a8c | |
|  | 1c93864567 | |
|  | 522d84f203 | |
|  | 6617b3bb28 | |
|  | c4800f4943 | |
|  | f0820b05c2 | |
|  | 1b2ff95c1e | |
|  | 4867df89a1 | |
|  | 4715a26af7 | |
|  | d366c1dd10 | |
|  | 2d0c280a0a | |
|  | c2cfd503ee | |
|  | 6c8806965e | |
|  | 8745c0598f | |
|  | 69b40ca560 | |
|  | c53591e9ec | |
|  | 24c844db49 | |
|  | 65f0758e82 | |
|  | 2f4b0b7aef | |
|  | 36e81104f1 | |
|  | fd835dd058 | |
|  | d886bcf755 | |
|  | 81b4187ae8 | |
|  | bbccb54059 | |
|  | 1e975f7b18 | |
|  | 457c946946 | |
|  | f84a337607 | |
|  | fa3e75f722 | |
|  | 1f3d331b25 | |
|  | 315fbcb18f | |
|  | 6fdf123f9f | |
|  | 4a43d8cfc7 | |
|  | adb8e42d61 | |
|  | 880c45025c | |
|  | 630f71ce4d | |
|  | e8576d3e94 | |
|  | ff28ebf753 | |
|  | 242b63317b | |
|  | a004602ce2 | |
|  | c749726a79 | |
|  | 761c27c0d3 | |
|  | fe91a91081 | |
|  | 0447439f99 | |
|  | ed1c305358 | |
|  | 13f2ca4fe4 | |
|  | 891e85b0bb | |
|  | c2f0fe6793 | |
|  | 7dfbe49996 | |
|  | ed0baf18d3 | |
|  | 8628985361 | |
|  | facddb0d6d | |
|  | 9797caa58e | |
|  | 9c896d9e0e | |
|  | bccd48014b | |
|  | 10083ff7af | |
|  | cfa8540be9 | |
|  | 3657dd70cf | |
|  | e88229bee2 | |
|  | 0b59072d9b | |
|  | 6542df9074 | |
|  | 40c646291e | |
|  | 4877354e8d | |
|  | 61d23cd8c2 | |
|  | 8dfd54eb9f | |
|  | af8d86321f | |
|  | 26817c1bb6 | |
|  | 6443eb9b00 | |
|  | 14eefb99e9 | |
|  | 3915ca6633 | |
|  | 5246c47ee6 | |
|  | 221a6e8139 | |
|  | b098096930 | |
|  | 47779baa5e | |
|  | eebdbc409c | |
|  | 95530ed5f0 | |
|  | 0a7f3737c5 | |
|  | 9cb7812144 | |
|  | 4625321079 | |
|  | 1c2c1a76fa | |
|  | 8d64bf7c6e | |
|  | 45181c11c5 | |
|  | e96480807c | |
|  | 9e94639657 | |
|  | 649a94c560 | |
|  | 8655258ac3 | |
|  | 67ba6b38c7 | |
|  | 16854f0f77 | |
|  | 582b5492fe | |
|  | 101f4c539a | |
|  | 522cded113 | |
|  | f77e15bf44 | |
|  | dbdbe1bf49 | |
|  | d3f0c90272 | |
|  | 566b16190e | |
|  | 5281102e36 | |
|  | b6420391e1 | |
|  | f8118315e7 | |
|  | d28d69d350 | |
|  | 2ca1714992 | |
|  | d72dd5fabc | |
|  | d253d87515 | |
|  | 9d3bb9ef04 | |
|  | f27f3fe62f | |
|  | 0d4251b321 | |
|  | 11fa58bd6e | |
|  | 71b0448004 | |
|  | eb0a603b8d | |
|  | 947a4e39c5 | |
|  | 7b7e44faf2 | |
|  | ac1581e8de | |
|  | e00599b4f0 | |
|  | 200f87ea48 | |
|  | 7676bcd1c1 | |
|  | 925df9b915 | |
|  | 7668ee2040 | |
|  | 90688016e8 | |
|  | b4fabb6d59 | |
|  | aa530c20d2 | |
|  | 69ce646bad | |
|  | 08c20fa2b9 | |
|  | e2daf22ad7 | |
|  | 921f310ac1 | |
|  | d9bee210d4 | |
|  | 2fc6940c11 | |
|  | ecad8e2801 | |
|  | 4a18c344c8 | |
|  | 58633313e1 | |
|  | 0f6dda44b8 | |
|  | b4b5a7ac8f | |
|  | a45e064c18 | |
|  | ecb4e0fab4 | |
|  | 035681ab28 | |
|  | 34779bb891 | |
|  | c61f42792f | |
|  | 788167e251 | |
|  | 019f31cc05 | |
|  | 91aca75138 | |
|  | 66fb6bf576 | |
|  | ad6ca25493 | |
|  | 4b4cac7cec | |
|  | 487c23da3e | |
|  | 4182ba6c1b | |
|  | 20094b5e42 | |
|  | 9d5f87d86f | |
|  | f0b487ca36 | |
|  | 5327bde032 | |
|  | c2f63f6121 | |
|  | 9d0056f0a6 | |
|  | a399103305 | |
|  | b7f8fce86e | |
|  | c77b07b8a2 | |
|  | 6fc3629014 | |
|  | 2da13af04c | |
|  | 363fbf2a6b | |
|  | 3953546ace | |
|  | b7e10363d0 | |
|  | f53a3eef05 | |
|  | ae8d84012b | |
|  | ddb86eae51 | |
|  | 144dd6e742 | |
|  | c465fbfdf4 | |
|  | beafdf29fb | |
|  | 00e2a38087 | |
|  | 80bf3ee2ed | |
|  | c32bbd518b | |
|  | 730a5c153e | |
|  | 3a9916e63b | |
|  | 3e9eb0d822 | |
|  | ef97dda39b | |
|  | 31f4a99d20 | |
|  | 759059baad | |
|  | cca0eb63a6 | |
|  | 6c37a082bf | |
|  | d2a9280d7d | |
|  | 64d19f61f2 | |
|  | cadc7b7750 | |
|  | d84c015787 | |
|  | 27a4dca7c6 | |
|  | 9c9a306f55 | |
|  | be77376d85 | |
|  | eecd74cc0f | |
|  | b4df4b785a | |
|  | 9b00e3d42c | |
|  | 170e885251 | |
|  | a96b203021 | |
|  | 057cc6dca5 | |
|  | 11d4118e71 | |
|  | f13cad57d8 | |
|  | b552a80203 | |
|  | b971a76662 | |
|  | 25da7331f0 | |
|  | 50b89f92ea | |
|  | 676e145349 | |
|  | f952257c20 | |
|  | e6e91b19d0 | |
|  | 26c7660bfa | |
|  | e50ac96b50 | |
|  | 20a39f5c29 | |
|  | 6e4657e90f | |
|  | 779d3e0bf6 | |
|  | a288d311c0 | |
|  | f87c42a746 | |
|  | 299327cf29 | |
|  | eb512c4c1b | |
|  | 7dfd50e19a | |
|  | dfdb24a550 | |
|  | e13bb7fc42 | |
|  | 828020d689 | |
|  | 4a8185839d | |
|  | 71d0984e9d | |
|  | 4e79b76377 | |
|  | fc16bea465 | |
|  | df200aae64 | |
|  | 06cc20fb2a | |
|  | 5a451115f4 | |
|  | fc71cdd7f8 | |
|  | e59920cfd0 | |
|  | 6e6f4f6694 | |
|  | 752f519ccc | |
|  | ffe08f913b | |
|  | 1f75f81297 | |
|  | b9e85c65bd | |
|  | e3b8cccba3 | |
|  | 5f9702848e | |
|  | 5dc419b7a7 | |
|  | aa2dcc027d | |
|  | f0b98d3063 | |
|  | 5b24d098e4 | |
|  | 53b3965a32 | |
|  | d0fa120202 | |
|  | fc1ed97499 | |
|  | e3f839bc56 | |
|  | d45ba62805 | |
|  | 5321942da8 | |
|  | 405f58124d | |
|  | 1e4ebae652 | |
|  | e932e4899c | |
|  | 7c8335d3e7 | |
|  | 81287a2c95 | |
|  | 9c3964da20 | |
|  | 3c9cce2c8b | |
|  | 35020a0108 | |
|  | e85292b58f | |
|  | 9fd2af6538 | |
|  | 949ce27f63 | |
|  | 81b66db3c6 | |
|  | b2fcaf6793 | 
|  | @ -0,0 +1,113 @@ | ||||||
|  | name: PHP Tests | ||||||
|  | 
 | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: [ main, master ] | ||||||
|  |   pull_request: | ||||||
|  |     branches: [ main, master ] | ||||||
|  | 
 | ||||||
|  | jobs: | ||||||
|  |   test: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  | 
 | ||||||
|  |     services: | ||||||
|  |       mariadb: | ||||||
|  |         image: mariadb:10.6 | ||||||
|  |         env: | ||||||
|  |           MARIADB_ROOT_PASSWORD: root | ||||||
|  |           MARIADB_DATABASE: jilo_test | ||||||
|  |           MARIADB_USER: test_jilo | ||||||
|  |           MARIADB_PASSWORD: test_password | ||||||
|  |         ports: | ||||||
|  |           - 3306:3306 | ||||||
|  |         options: >- | ||||||
|  |           --health-cmd="mysqladmin ping -h127.0.0.1 -P3306 -uroot -proot" | ||||||
|  |           --health-interval=10s | ||||||
|  |           --health-timeout=10s | ||||||
|  |           --health-retries=5 | ||||||
|  |           --health-start-period=30s | ||||||
|  | 
 | ||||||
|  |     steps: | ||||||
|  |     - uses: actions/checkout@v3 | ||||||
|  | 
 | ||||||
|  |     - name: Setup PHP | ||||||
|  |       uses: shivammathur/setup-php@v2 | ||||||
|  |       with: | ||||||
|  |         php-version: '8.2' | ||||||
|  |         extensions: pdo, pdo_mysql, xdebug | ||||||
|  |         coverage: xdebug | ||||||
|  | 
 | ||||||
|  |     - name: Wait for MariaDB | ||||||
|  |       run: | | ||||||
|  |         sudo apt-get 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 | ||||||
								
									
									
										
											59
										
									
									CHANGELOG.md
									
									
									
									
								
								
							
							
										
											59
										
									
									CHANGELOG.md
									
									
									
									
								|  | @ -4,6 +4,65 @@ All notable changes to this project will be documented in this file. | ||||||
| 
 | 
 | ||||||
| --- | --- | ||||||
| 
 | 
 | ||||||
|  | ## Unreleased | ||||||
|  | 
 | ||||||
|  | #### Links | ||||||
|  | - upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.4...HEAD | ||||||
|  | - codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.4...HEAD | ||||||
|  | - github: https://github.com/lindeas/jilo-web/compare/v0.4...HEAD | ||||||
|  | - gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.4...HEAD | ||||||
|  | 
 | ||||||
|  | ### Added | ||||||
|  | - Added CSS and JS to the default theme | ||||||
|  | - Added change theme menu entry | ||||||
|  | 
 | ||||||
|  | ### Changed | ||||||
|  | 
 | ||||||
|  | ### Fixed | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
|  | ## 0.4 - 2025-04-12 | ||||||
|  | 
 | ||||||
|  | #### Links | ||||||
|  | - upstream: https://code.lindeas.com/lindeas/jilo-web/compare/v0.3...v0.4 | ||||||
|  | - codeberg: https://codeberg.org/lindeas/jilo-web/compare/v0.3...v0.4 | ||||||
|  | - github: https://github.com/lindeas/jilo-web/compare/v0.3...v0.4 | ||||||
|  | - gitlab: https://gitlab.com/lindeas/jilo-web/-/compare/v0.3...v0.4 | ||||||
|  | 
 | ||||||
|  | ### Added | ||||||
|  | - Added top-right menu with profile, admin, and docs sections | ||||||
|  | - Added two-factor authentication | ||||||
|  | - Added resetting of forgotten password | ||||||
|  | - Added login credentials management | ||||||
|  | - Added proper pagination | ||||||
|  | - Added agents managemet pages | ||||||
|  | - Added javascript-based feedback messages | ||||||
|  | - Added description to each page | ||||||
|  | - Added CSRF checks | ||||||
|  | - Added validator class for all forms | ||||||
|  | - Added rate limiting to all pages | ||||||
|  | - Added authentication rate limiting to login and registration | ||||||
|  | - Added unit tests | ||||||
|  | - Added integration/feature tests | ||||||
|  | - Added testing workflow for github | ||||||
|  | 
 | ||||||
|  | ### Changed | ||||||
|  | - Increased session to 2 hours w/out "remember me", 30 days with | ||||||
|  | - Made the config editing in-place with AJAX | ||||||
|  | - Redesigned the help page | ||||||
|  | - Moved graphs and latest data to their own pages | ||||||
|  | - Moved live config.js to its own page | ||||||
|  | - Redesigned the messages system and renamed them to feedback messages | ||||||
|  | 
 | ||||||
|  | ### Fixed | ||||||
|  | - Bugfixes | ||||||
|  | - Fixed config editing | ||||||
|  | - Fixed logs search | ||||||
|  | - Removed hardcoded messages, changed to feedback messages | ||||||
|  | 
 | ||||||
|  | --- | ||||||
|  | 
 | ||||||
| ## 0.3 - 2025-01-15 | ## 0.3 - 2025-01-15 | ||||||
| 
 | 
 | ||||||
| #### Links | #### Links | ||||||
|  |  | ||||||
|  | @ -38,6 +38,8 @@ JQuery is used in this project and is licensed under the MIT License. See licens | ||||||
| 
 | 
 | ||||||
| Chart.js is used in this project and is licensed under the MIT License. See license-chartjs 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 | ## requirements | ||||||
| 
 | 
 | ||||||
| - web server (deb: apache | nginx) | - web server (deb: apache | nginx) | ||||||
|  |  | ||||||
|  | @ -24,29 +24,32 @@ class Agent { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Retrieves details of a specified agent ID (or all agents) in a specified platform. |      * Retrieves details of agents for a specified host. | ||||||
|      * |      * | ||||||
|      * @param int $platform_id The platform ID to filter agents by. |      * @param int $host_id The host ID to filter agents by. | ||||||
|      * @param int $agent_id The agent ID to filter by. If empty, all agents are returned. |      * @param int $agent_id Optional agent ID to filter by. | ||||||
|      * |      * | ||||||
|      * @return array The list of agent details. |      * @return array The list of agent details. | ||||||
|      */ |      */ | ||||||
|     public function getAgentDetails($platform_id, $agent_id = '') { |     public function getAgentDetails($host_id, $agent_id = '') { | ||||||
|         $sql = 'SELECT |         $sql = 'SELECT | ||||||
|                     ja.id, |                     ja.id, | ||||||
|                     ja.platform_id, |                     ja.host_id, | ||||||
|                     ja.agent_type_id, |                     ja.agent_type_id, | ||||||
|                     ja.url, |                     ja.url, | ||||||
|                     ja.secret_key, |                     ja.secret_key, | ||||||
|                     ja.check_period, |                     ja.check_period, | ||||||
|                     jat.description AS agent_description, |                     jat.description AS agent_description, | ||||||
|                     jat.endpoint AS agent_endpoint |                     jat.endpoint AS agent_endpoint, | ||||||
|  |                     h.platform_id | ||||||
|                 FROM |                 FROM | ||||||
|                     jilo_agents ja |                     jilo_agent ja | ||||||
|                 JOIN |                 JOIN | ||||||
|                     jilo_agent_types jat ON ja.agent_type_id = jat.id |                     jilo_agent_type jat ON ja.agent_type_id = jat.id | ||||||
|  |                 JOIN | ||||||
|  |                     host h ON ja.host_id = h.id | ||||||
|                 WHERE |                 WHERE | ||||||
|                     platform_id = :platform_id'; |                     ja.host_id = :host_id'; | ||||||
| 
 | 
 | ||||||
|         if ($agent_id !== '') { |         if ($agent_id !== '') { | ||||||
|             $sql .= ' AND ja.id = :agent_id'; |             $sql .= ' AND ja.id = :agent_id'; | ||||||
|  | @ -54,7 +57,7 @@ class Agent { | ||||||
| 
 | 
 | ||||||
|         $query = $this->db->prepare($sql); |         $query = $this->db->prepare($sql); | ||||||
| 
 | 
 | ||||||
|         $query->bindParam(':platform_id', $platform_id); |         $query->bindParam(':host_id', $host_id); | ||||||
|         if ($agent_id !== '') { |         if ($agent_id !== '') { | ||||||
|             $query->bindParam(':agent_id', $agent_id); |             $query->bindParam(':agent_id', $agent_id); | ||||||
|         } |         } | ||||||
|  | @ -75,17 +78,20 @@ class Agent { | ||||||
|     public function getAgentIDDetails($agent_id) { |     public function getAgentIDDetails($agent_id) { | ||||||
|         $sql = 'SELECT |         $sql = 'SELECT | ||||||
|                     ja.id, |                     ja.id, | ||||||
|                     ja.platform_id, |                     ja.host_id, | ||||||
|                     ja.agent_type_id, |                     ja.agent_type_id, | ||||||
|                     ja.url, |                     ja.url, | ||||||
|                     ja.secret_key, |                     ja.secret_key, | ||||||
|                     ja.check_period, |                     ja.check_period, | ||||||
|                     jat.description AS agent_description, |                     jat.description AS agent_description, | ||||||
|                     jat.endpoint AS agent_endpoint |                     jat.endpoint AS agent_endpoint, | ||||||
|  |                     h.platform_id | ||||||
|                 FROM |                 FROM | ||||||
|                     jilo_agents ja |                     jilo_agent ja | ||||||
|                 JOIN |                 JOIN | ||||||
|                     jilo_agent_types jat ON ja.agent_type_id = jat.id |                     jilo_agent_type jat ON ja.agent_type_id = jat.id | ||||||
|  |                 JOIN | ||||||
|  |                     host h ON ja.host_id = h.id | ||||||
|                 WHERE |                 WHERE | ||||||
|                     ja.id = :agent_id'; |                     ja.id = :agent_id'; | ||||||
| 
 | 
 | ||||||
|  | @ -104,7 +110,7 @@ class Agent { | ||||||
|      */ |      */ | ||||||
|     public function getAgentTypes() { |     public function getAgentTypes() { | ||||||
|         $sql = 'SELECT * |         $sql = 'SELECT * | ||||||
|                     FROM jilo_agent_types |                     FROM jilo_agent_type | ||||||
|                     ORDER BY id'; |                     ORDER BY id'; | ||||||
|         $query = $this->db->prepare($sql); |         $query = $this->db->prepare($sql); | ||||||
|         $query->execute(); |         $query->execute(); | ||||||
|  | @ -114,22 +120,22 @@ class Agent { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Retrieves agent types already configured for a specific platform. |      * Retrieves agent types already configured for a specific host. | ||||||
|      * |      * | ||||||
|      * @param int $platform_id The platform ID to filter agents by. |      * @param int $host_id The host ID to filter agents by. | ||||||
|      * |      * | ||||||
|      * @return array List of agent types configured for the platform. |      * @return array List of agent types configured for the host. | ||||||
|      */ |      */ | ||||||
|     public function getPlatformAgentTypes($platform_id) { |     public function getHostAgentTypes($host_id) { | ||||||
|         $sql = 'SELECT |         $sql = 'SELECT | ||||||
|                     id, |                     id, | ||||||
|                     agent_type_id |                     agent_type_id | ||||||
|                 FROM |                 FROM | ||||||
|                     jilo_agents |                     jilo_agent | ||||||
|                 WHERE |                 WHERE | ||||||
|                     platform_id = :platform_id'; |                     host_id = :host_id'; | ||||||
|         $query = $this->db->prepare($sql); |         $query = $this->db->prepare($sql); | ||||||
|         $query->bindParam(':platform_id', $platform_id); |         $query->bindParam(':host_id', $host_id); | ||||||
|         $query->execute(); |         $query->execute(); | ||||||
| 
 | 
 | ||||||
|         return $query->fetchAll(PDO::FETCH_ASSOC); |         return $query->fetchAll(PDO::FETCH_ASSOC); | ||||||
|  | @ -137,27 +143,27 @@ class Agent { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Adds a new agent to the platform. |      * Add a new agent to the database. | ||||||
|      * |      * | ||||||
|      * @param int $platform_id The platform ID where the agent is to be added. |      * @param int $host_id The host ID to add the agent to. | ||||||
|      * @param array $newAgent The new agent details to add. |      * @param array $newAgent An associative array containing the details of the agent to be added. | ||||||
|      * |      * | ||||||
|      * @return bool|string Returns true on success or an error message on failure. |      * @return bool|string True if the agent was added successfully, otherwise error message. | ||||||
|      */ |      */ | ||||||
|     public function addAgent($platform_id, $newAgent) { |     public function addAgent($host_id, $newAgent) { | ||||||
|         try { |         try { | ||||||
|             $sql = 'INSERT INTO jilo_agents |             $sql = 'INSERT INTO jilo_agent | ||||||
|                     (platform_id, agent_type_id, url, secret_key, check_period) |                     (host_id, agent_type_id, url, secret_key, check_period) | ||||||
|                     VALUES |                     VALUES | ||||||
|                     (:platform_id, :agent_type_id, :url, :secret_key, :check_period)'; |                     (:host_id, :agent_type_id, :url, :secret_key, :check_period)'; | ||||||
| 
 | 
 | ||||||
|             $query = $this->db->prepare($sql); |             $query = $this->db->prepare($sql); | ||||||
|             $query->execute([ |             $query->execute([ | ||||||
|                 ':platform_id'		=> $platform_id, |                 ':host_id'       => $host_id, | ||||||
|                 ':agent_type_id'	=> $newAgent['type_id'], |                 ':agent_type_id' => $newAgent['type_id'], | ||||||
|                 ':url'			=> $newAgent['url'], |                 ':url'          => $newAgent['url'], | ||||||
|                 ':secret_key'		=> $newAgent['secret_key'], |                 ':secret_key'   => $newAgent['secret_key'], | ||||||
|                 ':check_period'     => $newAgent['check_period'], |                 ':check_period' => $newAgent['check_period'], | ||||||
|             ]); |             ]); | ||||||
| 
 | 
 | ||||||
|             return true; |             return true; | ||||||
|  | @ -169,33 +175,34 @@ class Agent { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Edits an existing agent's details. |      * Edit an existing agent in the database. | ||||||
|      * |      * | ||||||
|      * @param int $platform_id The platform ID where the agent exists. |      * @param int $agent_id The ID of the agent to edit. | ||||||
|      * @param array $updatedAgent The updated agent details. |      * @param array $updatedAgent An associative array containing the updated details of the agent. | ||||||
|      * |      * | ||||||
|      * @return bool|string Returns true on success or an error message on failure. |      * @return bool|string True if the agent was updated successfully, otherwise error message. | ||||||
|      */ |      */ | ||||||
|     public function editAgent($platform_id, $updatedAgent) { |     public function editAgent($agent_id, $updatedAgent) { | ||||||
|         try { |         try { | ||||||
|             $sql = 'UPDATE jilo_agents SET |             $sql = 'UPDATE jilo_agent | ||||||
|  |                     SET | ||||||
|                         agent_type_id = :agent_type_id, |                         agent_type_id = :agent_type_id, | ||||||
|                         url = :url, |                         url = :url, | ||||||
|                         secret_key = :secret_key, |                         secret_key = :secret_key, | ||||||
|                         check_period = :check_period |                         check_period = :check_period | ||||||
|                     WHERE |                     WHERE | ||||||
|                         id = :agent_id |                         id = :agent_id'; | ||||||
|                     AND | 
 | ||||||
|                         platform_id = :platform_id'; |             // Convert empty secret key to NULL
 | ||||||
|  |             $secretKey = !empty($updatedAgent['secret_key']) ? $updatedAgent['secret_key'] : null; | ||||||
| 
 | 
 | ||||||
|             $query = $this->db->prepare($sql); |             $query = $this->db->prepare($sql); | ||||||
|             $query->execute([ |             $query->execute([ | ||||||
|                 ':agent_type_id'	=> $updatedAgent['agent_type_id'], |                 ':agent_id'      => $agent_id, | ||||||
|                 ':url'			=> $updatedAgent['url'], |                 ':agent_type_id' => $updatedAgent['agent_type_id'], | ||||||
|                 ':secret_key'		=> $updatedAgent['secret_key'], |                 ':url'           => $updatedAgent['url'], | ||||||
|                 ':check_period' => $updatedAgent['check_period'], |                 ':secret_key'    => $secretKey, | ||||||
|                 ':agent_id'		=> $updatedAgent['id'], |                 ':check_period'  => $updatedAgent['check_period'], | ||||||
|                 ':platform_id'		=> $platform_id, |  | ||||||
|             ]); |             ]); | ||||||
| 
 | 
 | ||||||
|             return true; |             return true; | ||||||
|  | @ -207,7 +214,7 @@ class Agent { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Deletes an agent from the platform. |      * Deletes an agent from the database. | ||||||
|      * |      * | ||||||
|      * @param int $agent_id The agent ID to delete. |      * @param int $agent_id The agent ID to delete. | ||||||
|      * |      * | ||||||
|  | @ -215,7 +222,7 @@ class Agent { | ||||||
|      */ |      */ | ||||||
|     public function deleteAgent($agent_id) { |     public function deleteAgent($agent_id) { | ||||||
|         try { |         try { | ||||||
|             $sql = 'DELETE FROM jilo_agents |             $sql = 'DELETE FROM jilo_agent | ||||||
|                     WHERE |                     WHERE | ||||||
|                     id = :agent_id'; |                     id = :agent_id'; | ||||||
| 
 | 
 | ||||||
|  | @ -279,7 +286,7 @@ class Agent { | ||||||
|         $base64Url_payload = $this->base64UrlEncode($payload); |         $base64Url_payload = $this->base64UrlEncode($payload); | ||||||
| 
 | 
 | ||||||
|         // signature
 |         // signature
 | ||||||
|         $signature = hash_hmac('sha256', $base64Url_header . "." . $base64Url_payload, $secret_key, true); |         $signature = hash_hmac('sha256', $base64Url_header . "." . $base64Url_payload, $secret_key ?? '', true); | ||||||
|         $base64Url_signature = $this->base64UrlEncode($signature); |         $base64Url_signature = $this->base64UrlEncode($signature); | ||||||
| 
 | 
 | ||||||
|         // build the JWT
 |         // build the JWT
 | ||||||
|  | @ -398,28 +405,30 @@ class Agent { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Retrieves the latest stored data for a specific platform, agent type, and metric type. |      * Retrieves the latest stored data for a specific host, agent type, and metric type. | ||||||
|      * |      * | ||||||
|      * @param int $platform_id The platform ID. |      * @param int $host_id The host ID. | ||||||
|      * @param string $agent_type The agent type. |      * @param string $agent_type The agent type. | ||||||
|      * @param string $metric_type The metric type to filter by. |      * @param string $metric_type The metric type to filter by. | ||||||
|      * |      * | ||||||
|      * @return mixed The latest stored data. |      * @return mixed The latest stored data. | ||||||
|      */ |      */ | ||||||
|     public function getLatestData($platform_id, $agent_type, $metric_type) { |     public function getLatestData($host_id, $agent_type, $metric_type) { | ||||||
|         $sql = 'SELECT |         $sql = 'SELECT | ||||||
|                     jac.timestamp, |                     jac.timestamp, | ||||||
|                     jac.response_content, |                     jac.response_content, | ||||||
|                     jac.agent_id, |                     jac.agent_id, | ||||||
|                     jat.description |                     jat.description | ||||||
|                 FROM |                 FROM | ||||||
|                     jilo_agent_checks jac |                     jilo_agent_check jac | ||||||
|                 JOIN |                 JOIN | ||||||
|                     jilo_agents ja ON jac.agent_id = ja.id |                     jilo_agent ja ON jac.agent_id = ja.id | ||||||
|                 JOIN |                 JOIN | ||||||
|                     jilo_agent_types jat ON ja.agent_type_id = jat.id |                     jilo_agent_type jat ON ja.agent_type_id = jat.id | ||||||
|  |                 JOIN | ||||||
|  |                     host h ON ja.host_id = h.id | ||||||
|                 WHERE |                 WHERE | ||||||
|                     ja.platform_id = :platform_id |                     h.id = :host_id | ||||||
|                     AND jat.description = :agent_type |                     AND jat.description = :agent_type | ||||||
|                     AND jac.status_code = 200 |                     AND jac.status_code = 200 | ||||||
|                 ORDER BY |                 ORDER BY | ||||||
|  | @ -428,7 +437,7 @@ class Agent { | ||||||
| 
 | 
 | ||||||
|         $query = $this->db->prepare($sql); |         $query = $this->db->prepare($sql); | ||||||
|         $query->execute([ |         $query->execute([ | ||||||
|             ':platform_id' => $platform_id, |             ':host_id' => $host_id, | ||||||
|             ':agent_type' => $agent_type |             ':agent_type' => $agent_type | ||||||
|         ]); |         ]); | ||||||
| 
 | 
 | ||||||
|  | @ -497,27 +506,29 @@ class Agent { | ||||||
|     /** |     /** | ||||||
|      * Gets historical data for a specific metric from agent checks |      * Gets historical data for a specific metric from agent checks | ||||||
|      * |      * | ||||||
|      * @param int $platform_id The platform ID |      * @param int $host_id The host ID | ||||||
|      * @param string $agent_type The type of agent (e.g., 'jvb', 'jicofo') |      * @param string $agent_type The type of agent (e.g., 'jvb', 'jicofo') | ||||||
|      * @param string $metric_type The type of metric to retrieve |      * @param string $metric_type The type of metric to retrieve | ||||||
|      * @param string $from_time Start time in Y-m-d format |      * @param string $from_time Start time in Y-m-d format | ||||||
|      * @param string $until_time End 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 |      * @return array Array with the dataset from agent checks | ||||||
|      */ |      */ | ||||||
|     public function getHistoricalData($platform_id, $agent_type, $metric_type, $from_time, $until_time) { |     public function getHistoricalData($host_id, $agent_type, $metric_type, $from_time, $until_time) { | ||||||
|         // Get data from agent checks
 |         // Get data from agent checks
 | ||||||
|         $sql = 'SELECT |         $sql = 'SELECT | ||||||
|                     DATE(jac.timestamp) as date, |                     DATE(jac.timestamp) as date, | ||||||
|                     jac.response_content, |                     jac.response_content, | ||||||
|                     COUNT(*) as checks_count |                     COUNT(*) as checks_count | ||||||
|                 FROM |                 FROM | ||||||
|                     jilo_agent_checks jac |                     jilo_agent_check jac | ||||||
|                 JOIN |                 JOIN | ||||||
|                     jilo_agents ja ON jac.agent_id = ja.id |                     jilo_agent ja ON jac.agent_id = ja.id | ||||||
|                 JOIN |                 JOIN | ||||||
|                     jilo_agent_types jat ON ja.agent_type_id = jat.id |                     jilo_agent_type jat ON ja.agent_type_id = jat.id | ||||||
|  |                 JOIN | ||||||
|  |                     host h ON ja.host_id = h.id | ||||||
|                 WHERE |                 WHERE | ||||||
|                     ja.platform_id = :platform_id |                     h.id = :host_id | ||||||
|                     AND jat.description = :agent_type |                     AND jat.description = :agent_type | ||||||
|                     AND jac.status_code = 200 |                     AND jac.status_code = 200 | ||||||
|                     AND DATE(jac.timestamp) BETWEEN :from_time AND :until_time |                     AND DATE(jac.timestamp) BETWEEN :from_time AND :until_time | ||||||
|  | @ -528,7 +539,7 @@ class Agent { | ||||||
| 
 | 
 | ||||||
|         $query = $this->db->prepare($sql); |         $query = $this->db->prepare($sql); | ||||||
|         $query->execute([ |         $query->execute([ | ||||||
|             ':platform_id' => $platform_id, |             ':host_id' => $host_id, | ||||||
|             ':agent_type' => $agent_type, |             ':agent_type' => $agent_type, | ||||||
|             ':from_time' => $from_time, |             ':from_time' => $from_time, | ||||||
|             ':until_time' => $until_time |             ':until_time' => $until_time | ||||||
|  | @ -565,6 +576,72 @@ class Agent { | ||||||
| 
 | 
 | ||||||
|         return $data; |         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; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,47 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * API Response Handler | ||||||
|  |  * Provides a consistent way to send JSON responses from controllers | ||||||
|  |  */ | ||||||
|  | class ApiResponse { | ||||||
|  |     /** | ||||||
|  |      * Send a success response | ||||||
|  |      * @param mixed $data Optional data to include in response | ||||||
|  |      * @param string $message Optional success message | ||||||
|  |      * @param int $status HTTP status code | ||||||
|  |      */ | ||||||
|  |     public static function success($data = null, $message = '', $status = 200) { | ||||||
|  |         self::send([ | ||||||
|  |             'success' => true, | ||||||
|  |             'data' => $data, | ||||||
|  |             'message' => $message | ||||||
|  |         ], $status); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Send an error response | ||||||
|  |      * @param string $message Error message | ||||||
|  |      * @param mixed $errors Optional error details | ||||||
|  |      * @param int $status HTTP status code | ||||||
|  |      */ | ||||||
|  |     public static function error($message, $errors = null, $status = 400) { | ||||||
|  |         self::send([ | ||||||
|  |             'success' => false, | ||||||
|  |             'error' => $message, | ||||||
|  |             'errors' => $errors | ||||||
|  |         ], $status); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Send the actual JSON response | ||||||
|  |      * @param array $data Response data | ||||||
|  |      * @param int $status HTTP status code | ||||||
|  |      */ | ||||||
|  |     private static function send($data, $status) { | ||||||
|  |         http_response_code($status); | ||||||
|  |         header('Content-Type: application/json'); | ||||||
|  |         echo json_encode($data); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -36,59 +36,134 @@ class Component { | ||||||
|      * @return array The list of Jitsi component events or an empty array if no results. |      * @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='') { |     public function jitsiComponents($jitsi_component, $component_id, $event_type, $from_time, $until_time, $offset=0, $items_per_page='') { | ||||||
|  |         global $logObject; | ||||||
|  |         try { | ||||||
|  |             // Add time part to dates if not present
 | ||||||
|  |             if (strlen($from_time) <= 10) { | ||||||
|  |                 $from_time .= ' 00:00:00'; | ||||||
|  |             } | ||||||
|  |             if (strlen($until_time) <= 10) { | ||||||
|  |                 $until_time .= ' 23:59:59'; | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|         // time period drill-down
 |             // list of jitsi component events
 | ||||||
|         // FIXME make it similar to the bash version
 |             $sql = "SELECT jitsi_component, loglevel, time, component_id, event_type, event_param
 | ||||||
|         if (empty($from_time)) { |                     FROM jitsi_components | ||||||
|             $from_time = '0000-01-01'; |                     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 []; | ||||||
|         } |         } | ||||||
|         if (empty($until_time)) { |  | ||||||
|             $until_time = '9999-12-31'; |  | ||||||
|         } |  | ||||||
|         $from_time = htmlspecialchars(strip_tags($from_time)); |  | ||||||
|         $until_time = htmlspecialchars(strip_tags($until_time)); |  | ||||||
| 
 |  | ||||||
|         // list of jitsi component events
 |  | ||||||
|         $sql = " |  | ||||||
| SELECT |  | ||||||
|     jitsi_component, loglevel, time, component_id, event_type, event_param |  | ||||||
| FROM |  | ||||||
|     jitsi_components |  | ||||||
| WHERE |  | ||||||
|     jitsi_component = %s |  | ||||||
| AND |  | ||||||
|     component_id = %s";
 |  | ||||||
|         if ($event_type != '' && $event_type != 'event_type') { |  | ||||||
|             $sql .= " |  | ||||||
| AND |  | ||||||
|     event_type LIKE '%%%s%%'";
 |  | ||||||
|         } |  | ||||||
|         $sql .= " |  | ||||||
| AND |  | ||||||
|     (time >= '%s 00:00:00' AND time <= '%s 23:59:59') |  | ||||||
| ORDER BY |  | ||||||
|     time";
 |  | ||||||
| 
 |  | ||||||
|         if ($items_per_page) { |  | ||||||
|             $items_per_page = (int)$items_per_page; |  | ||||||
|             $sql .= ' LIMIT ' . $offset . ',' . $items_per_page; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // FIXME this needs to be done with bound params instead of sprintf
 |  | ||||||
|         if ($event_type != '' && $event_type != 'event_type') { |  | ||||||
|             $sql = sprintf($sql, $jitsi_component, $component_id, $event_type, $from_time, $until_time); |  | ||||||
|             $sql = str_replace("LIKE '%'", "LIKE '%", $sql); |  | ||||||
|             $sql = str_replace("'%'\nAND", "%' AND", $sql); |  | ||||||
|         } else { |  | ||||||
|             $sql = sprintf($sql, $jitsi_component, $component_id, $from_time, $until_time); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         $query = $this->db->prepare($sql); |  | ||||||
|         $query->execute(); |  | ||||||
| 
 |  | ||||||
|         return $query->fetchAll(PDO::FETCH_ASSOC); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } |     /** | ||||||
|  |      * Gets the total count of components events matching the filter criteria | ||||||
|  |      *  | ||||||
|  |      * @param string $jitsi_component The Jitsi component name. | ||||||
|  |      * @param int $component_id The component ID. | ||||||
|  |      * @param string $event_type The type of event to filter by. | ||||||
|  |      * @param string $from_time The start date in 'YYYY-MM-DD' format. | ||||||
|  |      * @param string $until_time The end date in 'YYYY-MM-DD' format. | ||||||
|  |      *  | ||||||
|  |      * @return int The total count of matching components | ||||||
|  |      */ | ||||||
|  |     public function getComponentEventsCount($jitsi_component, $component_id, $event_type, $from_time, $until_time) { | ||||||
|  |         global $logObject; | ||||||
|  |         try { | ||||||
|  |             // Add time part to dates if not present
 | ||||||
|  |             if (strlen($from_time) <= 10) { | ||||||
|  |                 $from_time .= ' 00:00:00'; | ||||||
|  |             } | ||||||
|  |             if (strlen($until_time) <= 10) { | ||||||
|  |                 $until_time .= ' 23:59:59'; | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
| ?>
 |             // Build the query
 | ||||||
|  |             $sql = "SELECT COUNT(*) as total
 | ||||||
|  |                     FROM jitsi_components | ||||||
|  |                     WHERE time >= :from_time | ||||||
|  |                     AND time <= :until_time";
 | ||||||
|  | 
 | ||||||
|  |             // Only add component and event filters if they're not the default values
 | ||||||
|  |             if ($jitsi_component !== 'jitsi_component') { | ||||||
|  |                 $sql .= " AND LOWER(jitsi_component) = LOWER(:jitsi_component)"; | ||||||
|  |             } | ||||||
|  |             if ($component_id !== 'component_id') { | ||||||
|  |                 $sql .= " AND component_id = :component_id"; | ||||||
|  |             } | ||||||
|  |             if ($event_type !== 'event_type') { | ||||||
|  |                 $sql .= " AND event_type LIKE :event_type"; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $stmt = $this->db->prepare($sql); | ||||||
|  | 
 | ||||||
|  |             // Bind parameters only if they're not default values
 | ||||||
|  |             if ($jitsi_component !== 'jitsi_component') { | ||||||
|  |                 $stmt->bindValue(':jitsi_component', trim($jitsi_component, "'")); | ||||||
|  |             } | ||||||
|  |             if ($component_id !== 'component_id') { | ||||||
|  |                 $stmt->bindValue(':component_id', trim($component_id, "'")); | ||||||
|  |             } | ||||||
|  |             if ($event_type !== 'event_type') { | ||||||
|  |                 $stmt->bindValue(':event_type', '%' . trim($event_type, "'") . '%'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $stmt->bindParam(':from_time', $from_time); | ||||||
|  |             $stmt->bindParam(':until_time', $until_time); | ||||||
|  | 
 | ||||||
|  |             $stmt->execute(); | ||||||
|  |             $result = $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|  |             return (int)$result['total']; | ||||||
|  |         } catch (PDOException $e) { | ||||||
|  |             $logObject->log('error', "Failed to retrieve component events count: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']); | ||||||
|  |             return 0; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -382,5 +382,3 @@ SELECT COUNT(*) AS conferences | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -3,151 +3,156 @@ | ||||||
| /** | /** | ||||||
|  * class Config |  * class Config | ||||||
|  * |  * | ||||||
|  * Handles editing and fetching configuration files. |  * Handles editing and fetching of the config files. | ||||||
|  */ |  */ | ||||||
| class Config { | class Config { | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Edits a configuration file by updating specified options. |      * Edits a config file by updating specified options. | ||||||
|      * |      * | ||||||
|      * @param array $updatedConfig Key-value pairs of configuration options to update. |      * @param array $updatedConfig Key-value pairs of config options to update. | ||||||
|      * @param string $config_file Path to the configuration file. |      * @param string $config_file Path to the config file. | ||||||
|      * |      * | ||||||
|      * @return mixed Returns true on success, or an error message on failure. |      * @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) { |     public function editConfigFile($updatedConfig, $config_file) { | ||||||
|         // first we get a fresh config file contents as text
 |         global $logObject, $userId; | ||||||
|         $config_contents = file_get_contents($config_file); |         $allLogs = []; | ||||||
|         if (!$config_contents) { |         $updated = []; | ||||||
|             return "Failed to read the config file \"$config_file\".";
 |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         // loop through the variables and updated them
 |         try { | ||||||
|         foreach ($updatedConfig as $key => $newValue) { |             if (!is_array($updatedConfig)) { | ||||||
|             // we look for 'option' => value
 |                 throw new Exception("Invalid config data: expected array"); | ||||||
|             // option is always in single quotes
 |  | ||||||
|             // value is without quotes, because it could be true/false
 |  | ||||||
|             $pattern = "/(['\"]{$key}['\"]\s*=>\s*)([^,]+),/"; |  | ||||||
| 
 |  | ||||||
|             // prepare the value, make booleans w/out single quotes
 |  | ||||||
|             if ($newValue === 'true') { |  | ||||||
|                 $replacementValue = 'true'; |  | ||||||
|             } elseif ($newValue === 'false') { |  | ||||||
|                 $replacementValue = 'false'; |  | ||||||
|             } else { |  | ||||||
|                 $replacementValue = var_export($newValue, true); |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // value replacing
 |             if (!file_exists($config_file) || !is_writable($config_file)) { | ||||||
|             $config_contents = preg_replace($pattern, "$1{$replacementValue},", $config_contents); |                 throw new Exception("Config file does not exist or is not writable: $config_file"); | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // write the new config file
 |  | ||||||
|         if (!file_put_contents($config_file, $config_contents)) { |  | ||||||
|             return "Failed to write the config file \"$config_file\".";
 |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return true; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Loads the config.js file from the Jitsi server. |  | ||||||
|      * |  | ||||||
|      * @param string $jitsiUrl The base URL of the Jitsi server. |  | ||||||
|      * @param bool $raw Whether to return the full file (true) or only uncommented values (false). |  | ||||||
|      * |  | ||||||
|      * @return string The content of the config.js file or an error message. |  | ||||||
|      */ |  | ||||||
|     public function getPlatformConfigjs($jitsiUrl, $raw = false) { |  | ||||||
|         // constructing the URL
 |  | ||||||
|         $configjsFile = $jitsiUrl . '/config.js'; |  | ||||||
| 
 |  | ||||||
|         // default content, if we can't get the file contents
 |  | ||||||
|         $platformConfigjs = "The file $configjsFile can't be loaded."; |  | ||||||
| 
 |  | ||||||
|         // ssl options
 |  | ||||||
|         $contextOptions = [ |  | ||||||
|             'ssl' => [ |  | ||||||
|                 'verify_peer'		=> true, |  | ||||||
|                 'verify_peer_name'	=> true, |  | ||||||
|             ], |  | ||||||
|         ]; |  | ||||||
|         $context = stream_context_create($contextOptions); |  | ||||||
| 
 |  | ||||||
|         // get the file
 |  | ||||||
|         $fileContent = @file_get_contents($configjsFile, false, $context); |  | ||||||
| 
 |  | ||||||
|         if ($fileContent !== false) { |  | ||||||
| 
 |  | ||||||
|             // when we need only uncommented values
 |  | ||||||
|             if ($raw === false) { |  | ||||||
|                 // remove block comments
 |  | ||||||
|                 $platformConfigjs = preg_replace('!/\*.*?\*/!s', '', $fileContent); |  | ||||||
|                 // remove single-line comments
 |  | ||||||
|                 $platformConfigjs = preg_replace('/\/\/[^\n]*/', '', $platformConfigjs); |  | ||||||
|                 // remove empty lines
 |  | ||||||
|                 $platformConfigjs = preg_replace('/^\s*[\r\n]/m', '', $platformConfigjs); |  | ||||||
| 
 |  | ||||||
|             // when we need the full file as it is
 |  | ||||||
|             } else { |  | ||||||
|                 $platformConfigjs = $fileContent; |  | ||||||
|             } |             } | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         return $platformConfigjs; |             // 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"); | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Loads the interface_config.js file from the Jitsi server. |  | ||||||
|      * |  | ||||||
|      * @param string $jitsiUrl The base URL of the Jitsi server. |  | ||||||
|      * @param bool $raw Whether to return the full file (true) or only uncommented values (false). |  | ||||||
|      * |  | ||||||
|      * @return string The content of the interface_config.js file or an error message. |  | ||||||
|      */ |  | ||||||
|     public function getPlatformInterfaceConfigjs($jitsiUrl, $raw = false) { |  | ||||||
|         // constructing the URL
 |  | ||||||
|         $interfaceConfigjsFile = $jitsiUrl . '/interface_config.js'; |  | ||||||
| 
 |  | ||||||
|         // default content, if we can't get the file contents
 |  | ||||||
|         $platformInterfaceConfigjs = "The file $interfaceConfigjsFile can't be loaded."; |  | ||||||
| 
 |  | ||||||
|         // ssl options
 |  | ||||||
|         $contextOptions = [ |  | ||||||
|             'ssl' => [ |  | ||||||
|                 'verify_peer'		=> true, |  | ||||||
|                 'verify_peer_name'	=> true, |  | ||||||
|             ], |  | ||||||
|         ]; |  | ||||||
|         $context = stream_context_create($contextOptions); |  | ||||||
| 
 |  | ||||||
|         // get the file
 |  | ||||||
|         $fileContent = @file_get_contents($interfaceConfigjsFile, false, $context); |  | ||||||
| 
 |  | ||||||
|         if ($fileContent !== false) { |  | ||||||
| 
 |  | ||||||
|             // when we need only uncommented values
 |  | ||||||
|             if ($raw === false) { |  | ||||||
|                 // remove block comments
 |  | ||||||
|                 $platformInterfaceConfigjs = preg_replace('!/\*.*?\*/!s', '', $fileContent); |  | ||||||
|                 // remove single-line comments
 |  | ||||||
|                 $platformInterfaceConfigjs = preg_replace('/\/\/[^\n]*/', '', $platformInterfaceConfigjs); |  | ||||||
|                 // remove empty lines
 |  | ||||||
|                 $platformInterfaceConfigjs = preg_replace('/^\s*[\r\n]/m', '', $platformInterfaceConfigjs); |  | ||||||
| 
 |  | ||||||
|             // when we need the full file as it is
 |  | ||||||
|             } else { |  | ||||||
|                 $platformInterfaceConfigjs = $fileContent; |  | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             $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() | ||||||
|  |             ]; | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         return $platformInterfaceConfigjs; |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -28,13 +28,13 @@ class Database { | ||||||
|      */ |      */ | ||||||
|     public function __construct($options) { |     public function __construct($options) { | ||||||
|         // check if PDO extension is loaded
 |         // check if PDO extension is loaded
 | ||||||
|         if ( !extension_loaded('pdo') ) { |         if (!extension_loaded('pdo')) { | ||||||
|             $error = getError('PDO extension not loaded.'); |             throw new Exception('PDO extension not loaded.'); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // options check
 |         // options check
 | ||||||
|         if (empty($options['type'])) { |         if (empty($options['type'])) { | ||||||
|             $error = getError('Database type is not set.'); |             throw new Exception('Database type is not set.'); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // connect based on database type
 |         // connect based on database type
 | ||||||
|  | @ -42,15 +42,15 @@ class Database { | ||||||
|             case 'sqlite': |             case 'sqlite': | ||||||
|                 $this->connectSqlite($options); |                 $this->connectSqlite($options); | ||||||
|                 break; |                 break; | ||||||
|             case 'mysql' || 'mariadb': |             case 'mysql': | ||||||
|  |             case 'mariadb': | ||||||
|                 $this->connectMysql($options); |                 $this->connectMysql($options); | ||||||
|                 break; |                 break; | ||||||
|             default: |             default: | ||||||
|                 $error = getError("Database type \"{$options['type']}\" is not supported."); |                 $this->pdo = null; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Establishes a connection to a SQLite database. |      * Establishes a connection to a SQLite database. | ||||||
|      * |      * | ||||||
|  | @ -62,12 +62,17 @@ class Database { | ||||||
|     private function connectSqlite($options) { |     private function connectSqlite($options) { | ||||||
|         // pdo_sqlite extension is needed
 |         // pdo_sqlite extension is needed
 | ||||||
|         if (!extension_loaded('pdo_sqlite')) { |         if (!extension_loaded('pdo_sqlite')) { | ||||||
|             $error = getError('PDO extension for SQLite not loaded.'); |             throw new Exception('PDO extension for SQLite not loaded.'); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // SQLite options
 |         // SQLite options
 | ||||||
|         if (empty($options['dbFile']) || !file_exists($options['dbFile'])) { |         if (empty($options['dbFile'])) { | ||||||
|             $error = getError("SQLite database file \"{$options['dbFile']}\" not found."); |             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
 |         // connect to SQLite
 | ||||||
|  | @ -77,11 +82,10 @@ class Database { | ||||||
|             // enable foreign key constraints (not ON by default in SQLite3)
 |             // enable foreign key constraints (not ON by default in SQLite3)
 | ||||||
|             $this->pdo->exec('PRAGMA foreign_keys = ON;'); |             $this->pdo->exec('PRAGMA foreign_keys = ON;'); | ||||||
|         } catch (PDOException $e) { |         } catch (PDOException $e) { | ||||||
|             $error = getError('SQLite connection failed: ', $e->getMessage()); |             throw new Exception('SQLite connection failed: ' . $e->getMessage()); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Establishes a connection to a MySQL (or MariaDB) database. |      * Establishes a connection to a MySQL (or MariaDB) database. | ||||||
|      * |      * | ||||||
|  | @ -97,25 +101,25 @@ class Database { | ||||||
|     private function connectMysql($options) { |     private function connectMysql($options) { | ||||||
|         // pdo_mysql extension is needed
 |         // pdo_mysql extension is needed
 | ||||||
|         if (!extension_loaded('pdo_mysql')) { |         if (!extension_loaded('pdo_mysql')) { | ||||||
|             $error = getError('PDO extension for MySQL not loaded.'); |             throw new Exception('PDO extension for MySQL not loaded.'); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // MySQL options
 |         // MySQL options
 | ||||||
|         if (empty($options['host']) || empty($options['dbname']) || empty($options['user'])) { |         if (empty($options['host']) || empty($options['dbname']) || empty($options['user'])) { | ||||||
|             $error = getError('MySQL connection data is missing.'); |             throw new Exception('MySQL connection data is missing.'); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Connect to MySQL
 |         // Connect to MySQL
 | ||||||
|         try { |         try { | ||||||
|             $dsn = "mysql:host={$options['host']};port={$options['port']};dbname={$options['dbname']};charset=utf8"; |             $port = $options['port'] ?? 3306; | ||||||
|  |             $dsn = "mysql:host={$options['host']};port={$port};dbname={$options['dbname']};charset=utf8"; | ||||||
|             $this->pdo = new PDO($dsn, $options['user'], $options['password'] ?? ''); |             $this->pdo = new PDO($dsn, $options['user'], $options['password'] ?? ''); | ||||||
|             $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); |             $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); | ||||||
|         } catch (PDOException $e) { |         } catch (PDOException $e) { | ||||||
|             $error = getError('MySQL connection failed: ', $e->getMessage()); |             $this->pdo = null; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     /** |     /** | ||||||
|      * Retrieves the current PDO connection instance. |      * Retrieves the current PDO connection instance. | ||||||
|      * |      * | ||||||
|  | @ -125,6 +129,95 @@ class Database { | ||||||
|         return $this->pdo; |         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()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,13 +1,13 @@ | ||||||
| <?php | <?php | ||||||
| 
 | 
 | ||||||
| class Messages { | class Feedback { | ||||||
|     // Message types
 |     // Feedback types
 | ||||||
|     const TYPE_SUCCESS = 'success'; |     const TYPE_SUCCESS = 'success'; | ||||||
|     const TYPE_ERROR = 'danger'; |     const TYPE_ERROR = 'danger'; | ||||||
|     const TYPE_INFO = 'info'; |     const TYPE_INFO = 'info'; | ||||||
|     const TYPE_WARNING = 'warning'; |     const TYPE_WARNING = 'warning'; | ||||||
| 
 | 
 | ||||||
|     // Default message configurations
 |     // Default feedback message configurations
 | ||||||
|     const NOTICE = [ |     const NOTICE = [ | ||||||
|         'DEFAULT' => [ |         'DEFAULT' => [ | ||||||
|             'type' => self::TYPE_INFO, |             'type' => self::TYPE_INFO, | ||||||
|  | @ -35,6 +35,10 @@ class Messages { | ||||||
|             'type' => self::TYPE_SUCCESS, |             'type' => self::TYPE_SUCCESS, | ||||||
|             'dismissible' => true |             'dismissible' => true | ||||||
|         ], |         ], | ||||||
|  |         'SESSION_TIMEOUT' => [ | ||||||
|  |             'type' => self::TYPE_ERROR, | ||||||
|  |             'dismissible' => true | ||||||
|  |         ], | ||||||
|         'IP_BLACKLISTED' => [ |         'IP_BLACKLISTED' => [ | ||||||
|             'type' => self::TYPE_ERROR, |             'type' => self::TYPE_ERROR, | ||||||
|             'dismissible' => false |             'dismissible' => false | ||||||
|  | @ -49,6 +53,21 @@ class Messages { | ||||||
|         ] |         ] | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|  |     const REGISTER = [ | ||||||
|  |         'SUCCESS' => [ | ||||||
|  |             'type' => self::TYPE_SUCCESS, | ||||||
|  |             'dismissible' => true | ||||||
|  |         ], | ||||||
|  |         'FAILED' => [ | ||||||
|  |             'type' => self::TYPE_ERROR, | ||||||
|  |             'dismissible' => true | ||||||
|  |         ], | ||||||
|  |         'DISABLED' => [ | ||||||
|  |             'type' => self::TYPE_ERROR, | ||||||
|  |             'dismissible' => false | ||||||
|  |         ], | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|     const SECURITY = [ |     const SECURITY = [ | ||||||
|         'WHITELIST_ADD_SUCCESS' => [ |         'WHITELIST_ADD_SUCCESS' => [ | ||||||
|             'type' => self::TYPE_SUCCESS, |             'type' => self::TYPE_SUCCESS, | ||||||
|  | @ -96,20 +115,54 @@ class Messages { | ||||||
|         ] |         ] | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|  |     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; |     private static $strings = null; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get message strings |      * Get feedback message strings | ||||||
|      */ |      */ | ||||||
|     private static function getStrings() { |     private static function getStrings() { | ||||||
|         if (self::$strings === null) { |         if (self::$strings === null) { | ||||||
|             self::$strings = require __DIR__ . '/../includes/messages-strings.php'; |             self::$strings = require __DIR__ . '/../includes/strings.php'; | ||||||
|         } |         } | ||||||
|         return self::$strings; |         return self::$strings; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get message configuration by key |      * Get feedback message configuration by key | ||||||
|      */ |      */ | ||||||
|     public static function get($category, $key) { |     public static function get($category, $key) { | ||||||
|         $config = constant("self::$category")[$key] ?? null; |         $config = constant("self::$category")[$key] ?? null; | ||||||
|  | @ -122,9 +175,9 @@ class Messages { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Render message HTML |      * Render feedback message HTML | ||||||
|      */ |      */ | ||||||
|     // Usage: echo Messages::render('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
 |     // Usage: echo Feedback::render('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
 | ||||||
|     public static function render($category, $key, $customMessage = null, $dismissible = null, $small = false, $sanitize = true) { |     public static function render($category, $key, $customMessage = null, $dismissible = null, $small = false, $sanitize = true) { | ||||||
|         $config = self::get($category, $key); |         $config = self::get($category, $key); | ||||||
|         if (!$config) return ''; |         if (!$config) return ''; | ||||||
|  | @ -146,15 +199,30 @@ class Messages { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Store message in session for display after redirect |      * Get feedback message data for JavaScript | ||||||
|      */ |      */ | ||||||
|     // Usage: Messages::flash('LOGIN', 'LOGIN_SUCCESS', 'custom message [or null]', true [for dismissible; or null], true [for small; or omit]);
 |     public static function getMessageData($category, $key, $customMessage = null, $dismissible = null, $small = false) { | ||||||
|     public static function flash($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'])) { |         if (!isset($_SESSION['flash_messages'])) { | ||||||
|             $_SESSION['flash_messages'] = []; |             $_SESSION['flash_messages'] = []; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Get the message configuration
 |         // Get the feedback message configuration
 | ||||||
|         $config = self::get($category, $key); |         $config = self::get($category, $key); | ||||||
|         $isDismissible = $dismissible ?? $config['dismissible'] ?? false; |         $isDismissible = $dismissible ?? $config['dismissible'] ?? false; | ||||||
| 
 | 
 | ||||||
|  | @ -163,16 +231,17 @@ class Messages { | ||||||
|             'key' => $key, |             'key' => $key, | ||||||
|             'custom_message' => $customMessage, |             'custom_message' => $customMessage, | ||||||
|             'dismissible' => $isDismissible, |             'dismissible' => $isDismissible, | ||||||
|             'small' => $small |             'small' => $small, | ||||||
|  |             'sanitize' => $sanitize | ||||||
|         ]; |         ]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Get and clear all flash messages |      * Get and clear all flash feedback messages | ||||||
|      */ |      */ | ||||||
|     public static function getFlash() { |     public static function getFlash() { | ||||||
|         $messages = $_SESSION['flash_messages'] ?? []; |         $system_messages = $_SESSION['flash_messages'] ?? []; | ||||||
|         unset($_SESSION['flash_messages']); |         unset($_SESSION['flash_messages']); | ||||||
|         return $messages; |         return $system_messages; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -34,11 +34,10 @@ class Host { | ||||||
|         $sql = 'SELECT |         $sql = 'SELECT | ||||||
|                     id, |                     id, | ||||||
|                     address, |                     address, | ||||||
|                     port, |  | ||||||
|                     platform_id, |                     platform_id, | ||||||
|                     name |                     name | ||||||
|                 FROM |                 FROM | ||||||
|                     hosts'; |                     host'; | ||||||
| 
 | 
 | ||||||
|         if ($platform_id !== '' && $host_id !== '') { |         if ($platform_id !== '' && $host_id !== '') { | ||||||
|             $sql .= ' WHERE platform_id = :platform_id AND id = :host_id'; |             $sql .= ' WHERE platform_id = :platform_id AND id = :host_id'; | ||||||
|  | @ -72,15 +71,14 @@ class Host { | ||||||
|      */ |      */ | ||||||
|     public function addHost($newHost) { |     public function addHost($newHost) { | ||||||
|         try { |         try { | ||||||
|             $sql = 'INSERT INTO hosts |             $sql = 'INSERT INTO host | ||||||
|                     (address, port, platform_id, name) |                     (address, platform_id, name) | ||||||
|                     VALUES |                     VALUES | ||||||
|                     (:address, :port, :platform_id, :name)'; |                     (:address, :platform_id, :name)'; | ||||||
| 
 | 
 | ||||||
|             $query = $this->db->prepare($sql); |             $query = $this->db->prepare($sql); | ||||||
|             $query->execute([ |             $query->execute([ | ||||||
|                 ':address'          => $newHost['address'], |                 ':address'          => $newHost['address'], | ||||||
|                 ':port'             => $newHost['port'], |  | ||||||
|                 ':platform_id'		=> $newHost['platform_id'], |                 ':platform_id'		=> $newHost['platform_id'], | ||||||
|                 ':name'             => $newHost['name'], |                 ':name'             => $newHost['name'], | ||||||
|             ]); |             ]); | ||||||
|  | @ -99,25 +97,28 @@ class Host { | ||||||
|      * @param string $platform_id The platform ID to which the host belongs. |      * @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. |      * @param array $updatedHost An associative array containing the updated details of the host. | ||||||
|      * |      * | ||||||
|      * @return bool True if the host was updated successfully, otherwise false. |      * @return bool|string True if the host was updated successfully, otherwise error message. | ||||||
|      */ |      */ | ||||||
|     public function editHost($platform_id, $updatedHost) { |     public function editHost($platform_id, $updatedHost) { | ||||||
|         try { |         try { | ||||||
|             $sql = 'UPDATE hosts SET |             $sql = 'UPDATE host SET | ||||||
|                         address = :address, |                         address = :address, | ||||||
|                         port = :port, |  | ||||||
|                         name = :name |                         name = :name | ||||||
|                     WHERE |                     WHERE | ||||||
|                         id = :id'; |                         id = :id AND platform_id = :platform_id'; | ||||||
| 
 | 
 | ||||||
|             $query = $this->db->prepare($sql); |             $query = $this->db->prepare($sql); | ||||||
|             $query->execute([ |             $query->execute([ | ||||||
|                 ':id'       => $updatedHost['id'], |                 ':id'           => $updatedHost['id'], | ||||||
|                 ':address'  => $updatedHost['address'], |                 ':platform_id'  => $platform_id, | ||||||
|                 ':port'     => $updatedHost['port'], |                 ':address'      => $updatedHost['address'], | ||||||
|                 ':name'     => $updatedHost['name'], |                 ':name'         => $updatedHost['name'] | ||||||
|             ]); |             ]); | ||||||
| 
 | 
 | ||||||
|  |             if ($query->rowCount() === 0) { | ||||||
|  |                 return "No host found with ID {$updatedHost['id']} in platform $platform_id"; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             return true; |             return true; | ||||||
| 
 | 
 | ||||||
|         } catch (Exception $e) { |         } catch (Exception $e) { | ||||||
|  | @ -135,21 +136,30 @@ class Host { | ||||||
|      */ |      */ | ||||||
|     public function deleteHost($host_id) { |     public function deleteHost($host_id) { | ||||||
|         try { |         try { | ||||||
|             $sql = 'DELETE FROM hosts |             // Start transaction
 | ||||||
|                     WHERE |             $this->db->beginTransaction(); | ||||||
|                     id = :host_id'; |  | ||||||
| 
 | 
 | ||||||
|  |             // First delete all agents associated with this host
 | ||||||
|  |             $sql = 'DELETE FROM jilo_agent WHERE host_id = :host_id'; | ||||||
|             $query = $this->db->prepare($sql); |             $query = $this->db->prepare($sql); | ||||||
|             $query->bindParam(':host_id', $host_id); |             $query->bindParam(':host_id', $host_id); | ||||||
| 
 |  | ||||||
|             $query->execute(); |             $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; |             return true; | ||||||
| 
 | 
 | ||||||
|         } catch (Exception $e) { |         } catch (Exception $e) { | ||||||
|  |             // Rollback transaction on error
 | ||||||
|  |             $this->db->rollBack(); | ||||||
|             return $e->getMessage(); |             return $e->getMessage(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -1,95 +1,40 @@ | ||||||
| <?php | <?php | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * class Log |  * Log wrapper that delegates to plugin Log or NullLogger fallback. | ||||||
|  * |  * Used when code does require_once '../app/classes/log.php'. | ||||||
|  * Handles logging events into a database and reading log entries. |  | ||||||
|  */ |  */ | ||||||
| class Log { |  | ||||||
|     /** |  | ||||||
|      * @var PDO|null $db The database connection instance. |  | ||||||
|      */ |  | ||||||
|     private $db; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Logs constructor. |  | ||||||
|      * Initializes the database connection. |  | ||||||
|      * |  | ||||||
|      * @param object $database The database object to initialize the connection. |  | ||||||
|      */ |  | ||||||
|     public function __construct($database) { |  | ||||||
|         $this->db = $database->getConnection(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Insert a log event into the database. |  | ||||||
|      * |  | ||||||
|      * @param int    $user_id The ID of the user associated with the log event. |  | ||||||
|      * @param string $message The log message to insert. |  | ||||||
|      * @param string $scope   The scope of the log event (e.g., 'user', 'system'). Default is 'user'. |  | ||||||
|      * |  | ||||||
|      * @return bool|string True on success, or an error message on failure. |  | ||||||
|      */ |  | ||||||
|     public function insertLog($user_id, $message, $scope='user') { |  | ||||||
|         try { |  | ||||||
|             $sql = 'INSERT INTO logs |  | ||||||
|                         (user_id, scope, message) |  | ||||||
|                     VALUES |  | ||||||
|                         (:user_id, :scope, :message)'; |  | ||||||
| 
 |  | ||||||
|             $query = $this->db->prepare($sql); |  | ||||||
|             $query->execute([ |  | ||||||
|                 ':user_id'		=> $user_id, |  | ||||||
|                 ':scope'		=> $scope, |  | ||||||
|                 ':message'		=> $message, |  | ||||||
|             ]); |  | ||||||
| 
 |  | ||||||
|             return true; |  | ||||||
| 
 |  | ||||||
|         } catch (Exception $e) { |  | ||||||
|             return $e->getMessage(); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Retrieve log entries from the database. |  | ||||||
|      * |  | ||||||
|      * @param int    $user_id        The ID of the user whose logs are being retrieved. |  | ||||||
|      * @param string $scope          The scope of the logs ('user' or 'system'). |  | ||||||
|      * @param int    $offset         The offset for pagination. Default is 0. |  | ||||||
|      * @param int    $items_per_page The number of log entries to retrieve per page. Default is no limit. |  | ||||||
|      * |  | ||||||
|      * @return array An array of log entries. |  | ||||||
|      */ |  | ||||||
|     public function readLog($user_id, $scope, $offset=0, $items_per_page='') { |  | ||||||
|         if ($scope === 'user') { |  | ||||||
|             $sql = 'SELECT * FROM logs WHERE user_id = :user_id ORDER BY time DESC'; |  | ||||||
|             if ($items_per_page) { |  | ||||||
|                 $items_per_page = (int)$items_per_page; |  | ||||||
|                 $sql .= ' LIMIT ' . $offset . ',' . $items_per_page; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             $query = $this->db->prepare($sql); |  | ||||||
|             $query->execute([ |  | ||||||
|                 ':user_id'		=> $user_id, |  | ||||||
|             ]); |  | ||||||
|         } |  | ||||||
|         if ($scope === 'system') { |  | ||||||
|             $sql = 'SELECT * FROM logs ORDER BY time DESC'; |  | ||||||
|             if ($items_per_page) { |  | ||||||
|                 $items_per_page = (int)$items_per_page; |  | ||||||
|                 $sql .= ' LIMIT ' . $offset . ',' . $items_per_page; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             $query = $this->db->prepare($sql); |  | ||||||
|             $query->execute(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return $query->fetchAll(PDO::FETCH_ASSOC); |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|  | // 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 \App\Core\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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -372,5 +372,3 @@ AND pe.event_type = 'participant joining'"; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,172 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Handles password reset functionality including token generation and validation | ||||||
|  |  */ | ||||||
|  | class PasswordReset { | ||||||
|  |     private $db; | ||||||
|  |     private const TOKEN_LENGTH = 32; | ||||||
|  |     private const TOKEN_EXPIRY = 3600; // 1 hour
 | ||||||
|  | 
 | ||||||
|  |     public function __construct($database) { | ||||||
|  |         if ($database instanceof PDO) { | ||||||
|  |             $this->db = $database; | ||||||
|  |         } else { | ||||||
|  |             $this->db = $database->getConnection(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Creates a password reset request and sends email to user | ||||||
|  |      * | ||||||
|  |      * @param string $email User's email address | ||||||
|  |      * @return array Status of the reset request | ||||||
|  |      */ | ||||||
|  |     public function requestReset($email) { | ||||||
|  |         // Check if email exists
 | ||||||
|  |         $query = $this->db->prepare(" | ||||||
|  |             SELECT u.id, um.email | ||||||
|  |             FROM user u | ||||||
|  |             JOIN user_meta um ON u.id = um.user_id | ||||||
|  |             WHERE um.email = :email" | ||||||
|  |         ); | ||||||
|  |         $query->bindParam(':email', $email); | ||||||
|  |         $query->execute(); | ||||||
|  | 
 | ||||||
|  |         $user = $query->fetch(PDO::FETCH_ASSOC); | ||||||
|  |         if (!$user) { | ||||||
|  |             return ['success' => false, 'message' => 'If this email exists in our system, you will receive reset instructions.']; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Generate unique token
 | ||||||
|  |         $token = bin2hex(random_bytes(self::TOKEN_LENGTH / 2)); | ||||||
|  |         $expires = time() + self::TOKEN_EXPIRY; | ||||||
|  | 
 | ||||||
|  |         // Store token in database
 | ||||||
|  |         $query = $this->db->prepare(" | ||||||
|  |             INSERT INTO user_password_reset (user_id, token, expires) | ||||||
|  |             VALUES (:user_id, :token, :expires)" | ||||||
|  |         ); | ||||||
|  |         $query->bindParam(':user_id', $user['id']); | ||||||
|  |         $query->bindParam(':token', $token); | ||||||
|  |         $query->bindParam(':expires', $expires); | ||||||
|  | 
 | ||||||
|  |         if (!$query->execute()) { | ||||||
|  |             return ['success' => false, 'message' => 'Failed to process reset request']; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // We need the config for the email details
 | ||||||
|  |         global $config; | ||||||
|  | 
 | ||||||
|  |         // Prepare the reset link
 | ||||||
|  |         $scheme = $_SERVER['REQUEST_SCHEME']; | ||||||
|  |         $domain = trim($config['domain'], '/'); | ||||||
|  |         $folder = trim($config['folder'], '/'); | ||||||
|  |         $folderPath = $folder !== '' ? "/$folder" : ''; | ||||||
|  |         $resetLink = "{$scheme}://{$domain}{$folderPath}/index.php?page=login&action=reset&token=" . urlencode($token); | ||||||
|  | 
 | ||||||
|  |         // Send email with reset link
 | ||||||
|  |         $to = $user['email']; | ||||||
|  |         $subject = "{$config['site_name']} - Password reset request"; | ||||||
|  |         $message = "Dear user,\n\n"; | ||||||
|  |         $message .= "We received a request to reset your password for your {$config['site_name']} account.\n\n"; | ||||||
|  |         $message .= "To set a new password, please click the link below:\n\n"; | ||||||
|  |         $message .= $resetLink . "\n\n"; | ||||||
|  |         $message .= "This link will expire in 1 hour for security reasons.\n\n"; | ||||||
|  |         $message .= "If you did not request this password reset, please ignore this email. Your account remains secure.\n\n"; | ||||||
|  |         if (!empty($config['site_name'])) { | ||||||
|  |             $message .= "Best regards,\n"; | ||||||
|  |             $message .= "The {$config['site_name']} team\n"; | ||||||
|  |             if (!empty($config['site_slogan'])) { | ||||||
|  |                 $message .= ":: {$config['site_slogan']} ::"; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $headers = [ | ||||||
|  |             'From' => "noreply@{$config['domain']}", | ||||||
|  |             'Reply-To' => "noreply@{$config['domain']}", | ||||||
|  |             'X-Mailer' => 'PHP/' . phpversion() | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         if (!mail($to, $subject, $message, $headers)) { | ||||||
|  |             return ['success' => false, 'message' => 'Failed to send reset email']; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return ['success' => true, 'message' => 'If this email exists in our system, you will receive reset instructions.']; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Validates a reset token and returns associated user ID if valid | ||||||
|  |      * | ||||||
|  |      * @param string $token Reset token | ||||||
|  |      * @return array Validation result with user ID if successful | ||||||
|  |      */ | ||||||
|  |     public function validateToken($token) { | ||||||
|  |         $now = time(); | ||||||
|  | 
 | ||||||
|  |         $query = $this->db->prepare(" | ||||||
|  |             SELECT user_id | ||||||
|  |             FROM user_password_reset | ||||||
|  |             WHERE token = :token | ||||||
|  |             AND expires > :now | ||||||
|  |             AND used = 0 | ||||||
|  |         ");
 | ||||||
|  | 
 | ||||||
|  |         $query->bindParam(':token', $token); | ||||||
|  |         $query->bindParam(':now', $now); | ||||||
|  |         $query->execute(); | ||||||
|  | 
 | ||||||
|  |         $result = $query->fetch(PDO::FETCH_ASSOC); | ||||||
|  | 
 | ||||||
|  |         if (!$result) { | ||||||
|  |             return ['valid' => false]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return ['valid' => true, 'user_id' => $result['user_id']]; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Completes the password reset process | ||||||
|  |      * | ||||||
|  |      * @param string $token Reset token | ||||||
|  |      * @param string $newPassword New password | ||||||
|  |      * @return bool Whether reset was successful | ||||||
|  |      */ | ||||||
|  |     public function resetPassword($token, $newPassword) { | ||||||
|  |         $validation = $this->validateToken($token); | ||||||
|  |         if (!$validation['valid']) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Start transaction
 | ||||||
|  |         $this->db->beginTransaction(); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             // Update password
 | ||||||
|  |             $hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT); | ||||||
|  |             $query = $this->db->prepare( | ||||||
|  |                 "UPDATE user
 | ||||||
|  |                 SET password = :password | ||||||
|  |                 WHERE id = :user_id" | ||||||
|  |             ); | ||||||
|  |             $query->bindParam(':password', $hashedPassword); | ||||||
|  |             $query->bindParam(':user_id', $validation['user_id']); | ||||||
|  |             $query->execute(); | ||||||
|  | 
 | ||||||
|  |             // Mark token as used
 | ||||||
|  |             $query = $this->db->prepare( | ||||||
|  |                 "UPDATE user_password_reset
 | ||||||
|  |                 SET used = 1 | ||||||
|  |                 WHERE token = :token" | ||||||
|  |             ); | ||||||
|  |             $query->bindParam(':token', $token); | ||||||
|  |             $query->execute(); | ||||||
|  | 
 | ||||||
|  |             $this->db->commit(); | ||||||
|  |             return true; | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             $this->db->rollBack(); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -30,7 +30,7 @@ class Platform { | ||||||
|      * @return array An associative array containing platform details. |      * @return array An associative array containing platform details. | ||||||
|      */ |      */ | ||||||
|     public function getPlatformDetails($platform_id = '') { |     public function getPlatformDetails($platform_id = '') { | ||||||
|         $sql = 'SELECT * FROM platforms'; |         $sql = 'SELECT * FROM platform'; | ||||||
|         if ($platform_id !== '') { |         if ($platform_id !== '') { | ||||||
|             $sql .= ' WHERE id = :platform_id'; |             $sql .= ' WHERE id = :platform_id'; | ||||||
|             $query = $this->db->prepare($sql); |             $query = $this->db->prepare($sql); | ||||||
|  | @ -57,7 +57,7 @@ class Platform { | ||||||
|      */ |      */ | ||||||
|     public function addPlatform($newPlatform) { |     public function addPlatform($newPlatform) { | ||||||
|         try { |         try { | ||||||
|             $sql = 'INSERT INTO platforms |             $sql = 'INSERT INTO platform | ||||||
|                     (name, jitsi_url, jilo_database) |                     (name, jitsi_url, jilo_database) | ||||||
|                     VALUES |                     VALUES | ||||||
|                     (:name, :jitsi_url, :jilo_database)'; |                     (:name, :jitsi_url, :jilo_database)'; | ||||||
|  | @ -90,7 +90,7 @@ class Platform { | ||||||
|      */ |      */ | ||||||
|     public function editPlatform($platform_id, $updatedPlatform) { |     public function editPlatform($platform_id, $updatedPlatform) { | ||||||
|         try { |         try { | ||||||
|             $sql = 'UPDATE platforms SET |             $sql = 'UPDATE platform SET | ||||||
|                         name = :name, |                         name = :name, | ||||||
|                         jitsi_url = :jitsi_url, |                         jitsi_url = :jitsi_url, | ||||||
|                         jilo_database = :jilo_database |                         jilo_database = :jilo_database | ||||||
|  | @ -122,21 +122,42 @@ class Platform { | ||||||
|      */ |      */ | ||||||
|     public function deletePlatform($platform_id) { |     public function deletePlatform($platform_id) { | ||||||
|         try { |         try { | ||||||
|             $sql = 'DELETE FROM platforms |             $this->db->beginTransaction(); | ||||||
|                     WHERE |  | ||||||
|                     id = :platform_id'; |  | ||||||
| 
 | 
 | ||||||
|  |             // First, get all hosts in this platform
 | ||||||
|  |             $sql = 'SELECT id FROM host WHERE platform_id = :platform_id'; | ||||||
|             $query = $this->db->prepare($sql); |             $query = $this->db->prepare($sql); | ||||||
|             $query->bindParam(':platform_id', $platform_id); |             $query->bindParam(':platform_id', $platform_id); | ||||||
| 
 |  | ||||||
|             $query->execute(); |             $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; |             return true; | ||||||
| 
 | 
 | ||||||
|         } catch (Exception $e) { |         } catch (Exception $e) { | ||||||
|  |             $this->db->rollBack(); | ||||||
|             return $e->getMessage(); |             return $e->getMessage(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -1,53 +1,96 @@ | ||||||
| <?php | <?php | ||||||
| 
 | 
 | ||||||
|  | use App\Core\NullLogger; | ||||||
|  | 
 | ||||||
| class RateLimiter { | class RateLimiter { | ||||||
|     public $db; |     public $db; | ||||||
|     private $log; |     private $database; | ||||||
|  |     /** @var mixed NullLogger (or PSR-3 logger) or plugin Log */ | ||||||
|  |     private $logger; | ||||||
|     public $maxAttempts = 5;           // Maximum login attempts
 |     public $maxAttempts = 5;           // Maximum login attempts
 | ||||||
|     public $decayMinutes = 15;         // Time window in minutes
 |     public $decayMinutes = 15;         // Time window in minutes
 | ||||||
|     public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
 |     public $autoBlacklistThreshold = 10; // Attempts before auto-blacklist
 | ||||||
|     public $autoBlacklistDuration = 24;  // Hours to blacklist for
 |     public $autoBlacklistDuration = 24;  // Hours to blacklist for
 | ||||||
|     public $ratelimitTable = 'login_attempts'; |     public $authRatelimitTable = 'security_rate_auth';  // For rate limiting username/password attempts
 | ||||||
|     public $whitelistTable = 'ip_whitelist'; |     public $pagesRatelimitTable = 'security_rate_page';  // For rate limiting page requests
 | ||||||
|     public $blacklistTable = 'ip_blacklist'; |     public $whitelistTable = 'security_ip_whitelist';  // For whitelisting IPs and network ranges
 | ||||||
|  |     public $blacklistTable = 'security_ip_blacklist';  // For blacklisting IPs and network ranges
 | ||||||
|  |     private $pageLimits = [ | ||||||
|  |         // Default rate limits per minute
 | ||||||
|  |         'default' => 60, | ||||||
|  |         'admin' => 120, | ||||||
|  |         'message' => 20, | ||||||
|  |         'contact' => 30, | ||||||
|  |         'call' => 30, | ||||||
|  |         'register' => 5, | ||||||
|  |         'config' => 10 | ||||||
|  |     ]; | ||||||
| 
 | 
 | ||||||
|     public function __construct($database) { |     /** | ||||||
|  |      * @param mixed $database Database object | ||||||
|  |      * @param mixed $logger Optional NullLogger (or PSR-3 logger) or plugin Log | ||||||
|  |      */ | ||||||
|  |     public function __construct($database, $logger = null) { | ||||||
|  |         $this->database = $database; | ||||||
|         $this->db = $database->getConnection(); |         $this->db = $database->getConnection(); | ||||||
|         $this->log = new Log($database); |         // 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(); |         $this->createTablesIfNotExist(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // Database preparation
 |     // Database preparation
 | ||||||
|     private function createTablesIfNotExist() { |     private function createTablesIfNotExist() { | ||||||
|         // Login attempts table
 |         // Authentication attempts table
 | ||||||
|         $sql = "CREATE TABLE IF NOT EXISTS {$this->ratelimitTable} (
 |         $sql = "CREATE TABLE IF NOT EXISTS {$this->authRatelimitTable} (
 | ||||||
|             id INTEGER PRIMARY KEY AUTOINCREMENT, |             id int(11) PRIMARY KEY AUTO_INCREMENT, | ||||||
|             ip_address TEXT NOT NULL UNIQUE, |             ip_address VARCHAR(45) NOT NULL, | ||||||
|             username TEXT NOT NULL, |             username VARCHAR(255) NOT NULL, | ||||||
|             attempted_at TEXT DEFAULT (DATETIME('now')) |             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); |         $this->db->exec($sql); | ||||||
| 
 | 
 | ||||||
|         // IP whitelist table
 |         // IP whitelist table
 | ||||||
|         $sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
 |         $sql = "CREATE TABLE IF NOT EXISTS {$this->whitelistTable} (
 | ||||||
|             id INTEGER PRIMARY KEY AUTOINCREMENT, |             id int(11) PRIMARY KEY AUTO_INCREMENT, | ||||||
|             ip_address TEXT NOT NULL UNIQUE, |             ip_address VARCHAR(45) NOT NULL, | ||||||
|             is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)), |             is_network BOOLEAN DEFAULT FALSE, | ||||||
|             description TEXT, |             description VARCHAR(255), | ||||||
|             created_at TEXT DEFAULT (DATETIME('now')), |             created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|             created_by TEXT |             created_by VARCHAR(255), | ||||||
|  |             UNIQUE KEY unique_ip (ip_address) | ||||||
|         )";
 |         )";
 | ||||||
|         $this->db->exec($sql); |         $this->db->exec($sql); | ||||||
| 
 | 
 | ||||||
|         // IP blacklist table
 |         // IP blacklist table
 | ||||||
|         $sql = "CREATE TABLE IF NOT EXISTS {$this->blacklistTable} (
 |         $sql = "CREATE TABLE IF NOT EXISTS {$this->blacklistTable} (
 | ||||||
|             id INTEGER PRIMARY KEY AUTOINCREMENT, |             id int(11) PRIMARY KEY AUTO_INCREMENT, | ||||||
|             ip_address TEXT NOT NULL UNIQUE, |             ip_address VARCHAR(45) NOT NULL, | ||||||
|             is_network BOOLEAN DEFAULT 0 CHECK(is_network IN (0,1)), |             is_network BOOLEAN DEFAULT FALSE, | ||||||
|             reason TEXT, |             reason VARCHAR(255), | ||||||
|             expiry_time TEXT  NULL, |             expiry_time TIMESTAMP NULL, | ||||||
|             created_at TEXT DEFAULT (DATETIME('now')), |             created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|             created_by TEXT |             created_by VARCHAR(255), | ||||||
|  |             UNIQUE KEY unique_ip (ip_address) | ||||||
|         )";
 |         )";
 | ||||||
|         $this->db->exec($sql); |         $this->db->exec($sql); | ||||||
| 
 | 
 | ||||||
|  | @ -61,7 +104,7 @@ class RateLimiter { | ||||||
|         ]; |         ]; | ||||||
| 
 | 
 | ||||||
|         // Insert default whitelisted IPs if they don't exist
 |         // Insert default whitelisted IPs if they don't exist
 | ||||||
|         $stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->whitelistTable} |         $stmt = $this->db->prepare("INSERT IGNORE INTO {$this->whitelistTable} | ||||||
|             (ip_address, is_network, description, created_by) |             (ip_address, is_network, description, created_by) | ||||||
|             VALUES (?, ?, ?, 'system')");
 |             VALUES (?, ?, ?, 'system')");
 | ||||||
|         foreach ($defaultIps as $ip) { |         foreach ($defaultIps as $ip) { | ||||||
|  | @ -77,7 +120,7 @@ class RateLimiter { | ||||||
|             ['203.0.113.0/24', true, 'TEST-NET-3 Documentation space - RFC 5737'] |             ['203.0.113.0/24', true, 'TEST-NET-3 Documentation space - RFC 5737'] | ||||||
|         ]; |         ]; | ||||||
| 
 | 
 | ||||||
|         $stmt = $this->db->prepare("INSERT OR IGNORE INTO {$this->blacklistTable} |         $stmt = $this->db->prepare("INSERT IGNORE INTO {$this->blacklistTable} | ||||||
|             (ip_address, is_network, reason, created_by) |             (ip_address, is_network, reason, created_by) | ||||||
|             VALUES (?, ?, ?, 'system')");
 |             VALUES (?, ?, ?, 'system')");
 | ||||||
| 
 | 
 | ||||||
|  | @ -91,8 +134,8 @@ class RateLimiter { | ||||||
|      * Get number of recent login attempts for an IP |      * Get number of recent login attempts for an IP | ||||||
|      */ |      */ | ||||||
|     public function getRecentAttempts($ip) { |     public function getRecentAttempts($ip) { | ||||||
|         $stmt = $this->db->prepare("SELECT COUNT(*) as attempts FROM {$this->ratelimitTable} 
 |         $stmt = $this->db->prepare("SELECT COUNT(*) as attempts FROM {$this->authRatelimitTable} | ||||||
|             WHERE ip_address = ? AND attempted_at > datetime('now', '-' || :minutes || ' minutes')");
 |             WHERE ip_address = ? AND attempted_at > DATE_SUB(NOW(), INTERVAL ? MINUTE)");
 | ||||||
|         $stmt->execute([$ip, $this->decayMinutes]); |         $stmt->execute([$ip, $this->decayMinutes]); | ||||||
|         $result = $stmt->fetch(PDO::FETCH_ASSOC); |         $result = $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|         return intval($result['attempts']); |         return intval($result['attempts']); | ||||||
|  | @ -103,7 +146,20 @@ class RateLimiter { | ||||||
|      */ |      */ | ||||||
|     public function isIpBlacklisted($ip) { |     public function isIpBlacklisted($ip) { | ||||||
|         // First check if IP is explicitly blacklisted or in a blacklisted range
 |         // First check if IP is explicitly blacklisted or in a blacklisted range
 | ||||||
|         $stmt = $this->db->prepare("SELECT ip_address, is_network, expiry_time FROM {$this->blacklistTable}"); |         $stmt = $this->db->prepare("SELECT ip_address, is_network, expiry_time FROM {$this->blacklistTable} WHERE ip_address = ?"); | ||||||
|  |         $stmt->execute([$ip]); | ||||||
|  |         $row = $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|  | 
 | ||||||
|  |         if ($row) { | ||||||
|  |             // Skip expired entries
 | ||||||
|  |             if ($row['expiry_time'] !== null && strtotime($row['expiry_time']) < time()) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Check network ranges
 | ||||||
|  |         $stmt = $this->db->prepare("SELECT ip_address, expiry_time FROM {$this->blacklistTable} WHERE is_network = 1"); | ||||||
|         $stmt->execute(); |         $stmt->execute(); | ||||||
| 
 | 
 | ||||||
|         while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { |         while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { | ||||||
|  | @ -112,14 +168,8 @@ class RateLimiter { | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if ($row['is_network']) { |             if ($this->ipInRange($ip, $row['ip_address'])) { | ||||||
|                 if ($this->ipInRange($ip, $row['ip_address'])) { |                 return true; | ||||||
|                     return true; |  | ||||||
|                 } |  | ||||||
|             } else { |  | ||||||
|                 if ($ip === $row['ip_address']) { |  | ||||||
|                     return true; |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -130,19 +180,24 @@ class RateLimiter { | ||||||
|      * Check if an IP is whitelisted |      * Check if an IP is whitelisted | ||||||
|      */ |      */ | ||||||
|     public function isIpWhitelisted($ip) { |     public function isIpWhitelisted($ip) { | ||||||
|         // Check exact IP match and CIDR ranges
 |         // Check exact IP match first
 | ||||||
|         $stmt = $this->db->prepare("SELECT ip_address, is_network FROM {$this->whitelistTable}"); |         $stmt = $this->db->prepare("SELECT ip_address FROM {$this->whitelistTable} WHERE ip_address = ?"); | ||||||
|         $stmt->execute(); |         $stmt->execute([$ip]); | ||||||
|  |         $row = $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|  |         if ($row) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { |         // Only check ranges for IPv4 addresses
 | ||||||
|             if ($row['is_network']) { |         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'])) { |                 if ($this->ipInRange($ip, $row['ip_address'])) { | ||||||
|                     return true; |                     return true; | ||||||
|                 } |                 } | ||||||
|             } else { |  | ||||||
|                 if ($ip === $row['ip_address']) { |  | ||||||
|                     return true; |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -150,8 +205,18 @@ class RateLimiter { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private function ipInRange($ip, $cidr) { |     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); |         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); |         $ip = ip2long($ip); | ||||||
|         $subnet = ip2long($subnet); |         $subnet = ip2long($subnet); | ||||||
|         $mask = -1 << (32 - $bits); |         $mask = -1 << (32 - $bits); | ||||||
|  | @ -167,15 +232,19 @@ class RateLimiter { | ||||||
|             if ($this->isIpBlacklisted($ip)) { |             if ($this->isIpBlacklisted($ip)) { | ||||||
|                 $message = "Cannot whitelist {$ip} - IP is currently blacklisted"; |                 $message = "Cannot whitelist {$ip} - IP is currently blacklisted"; | ||||||
|                 if ($userId) { |                 if ($userId) { | ||||||
|                     $this->log->insertLog($userId, "IP Whitelist: {$message}", 'system'); |                     $this->logger->log('info', "IP Whitelist: {$message}", ['user_id' => $userId, 'scope' => 'system']); | ||||||
|                     Messages::flash('ERROR', 'DEFAULT', $message); |                     Feedback::flash('ERROR', 'DEFAULT', $message); | ||||||
|                 } |                 } | ||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             $stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->whitelistTable} |             $stmt = $this->db->prepare("INSERT INTO {$this->whitelistTable} | ||||||
|                 (ip_address, is_network, description, created_by) |                 (ip_address, is_network, description, created_by) | ||||||
|                 VALUES (?, ?, ?, ?)");
 |                 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]); |                 $result = $stmt->execute([$ip, $isNetwork, $description, $createdBy]); | ||||||
| 
 | 
 | ||||||
|  | @ -187,15 +256,15 @@ class RateLimiter { | ||||||
|                         $createdBy, |                         $createdBy, | ||||||
|                         $description |                         $description | ||||||
|                     ); |                     ); | ||||||
|                     $this->log->insertLog($userId ?? 0, $logMessage, 'system'); |                     $this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|             return $result; |             return $result; | ||||||
| 
 | 
 | ||||||
|         } catch (Exception $e) { |         } catch (Exception $e) { | ||||||
|             if ($userId) { |             if ($userId) { | ||||||
|                 $this->log->insertLog($userId, "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), 'system'); |                 $this->logger->log('error', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']); | ||||||
|                 Messages::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage()); |                 Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to add {$ip}: " . $e->getMessage()); | ||||||
|             } |             } | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  | @ -222,15 +291,15 @@ class RateLimiter { | ||||||
|                     $removedBy, |                     $removedBy, | ||||||
|                     $ipDetails['created_by'] |                     $ipDetails['created_by'] | ||||||
|                 ); |                 ); | ||||||
|                 $this->log->insertLog($userId ?? 0, $logMessage, 'system'); |                 $this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return $result; |             return $result; | ||||||
| 
 | 
 | ||||||
|         } catch (Exception $e) { |         } catch (Exception $e) { | ||||||
|             if ($userId) { |             if ($userId) { | ||||||
|                 $this->log->insertLog($userId, "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), 'system'); |                 $this->logger->log('error', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']); | ||||||
|                 Messages::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage()); |                 Feedback::flash('ERROR', 'DEFAULT', "IP Whitelist: Failed to remove {$ip}: " . $e->getMessage()); | ||||||
|             } |             } | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  | @ -242,17 +311,22 @@ class RateLimiter { | ||||||
|             if ($this->isIpWhitelisted($ip)) { |             if ($this->isIpWhitelisted($ip)) { | ||||||
|                 $message = "Cannot blacklist {$ip} - IP is currently whitelisted"; |                 $message = "Cannot blacklist {$ip} - IP is currently whitelisted"; | ||||||
|                 if ($userId) { |                 if ($userId) { | ||||||
|                     $this->log->insertLog($userId, "IP Blacklist: {$message}", 'system'); |                     $this->logger->log('info', "IP Blacklist: {$message}", ['user_id' => $userId, 'scope' => 'system']); | ||||||
|                     Messages::flash('ERROR', 'DEFAULT', $message); |                     Feedback::flash('ERROR', 'DEFAULT', $message); | ||||||
|                 } |                 } | ||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             $expiryTime = $expiryHours ? date('Y-m-d H:i:s', strtotime("+{$expiryHours} hours")) : null; |             $expiryTime = $expiryHours ? date('Y-m-d H:i:s', strtotime("+{$expiryHours} hours")) : null; | ||||||
| 
 | 
 | ||||||
|             $stmt = $this->db->prepare("INSERT OR REPLACE INTO {$this->blacklistTable} |             $stmt = $this->db->prepare("INSERT INTO {$this->blacklistTable} | ||||||
|                 (ip_address, is_network, reason, expiry_time, created_by) |                 (ip_address, is_network, reason, expiry_time, created_by) | ||||||
|                 VALUES (?, ?, ?, ?, ?)");
 |                 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]); |             $result = $stmt->execute([$ip, $isNetwork, $reason, $expiryTime, $createdBy]); | ||||||
| 
 | 
 | ||||||
|  | @ -265,14 +339,14 @@ class RateLimiter { | ||||||
|                     $reason, |                     $reason, | ||||||
|                     $expiryTime ?? 'never' |                     $expiryTime ?? 'never' | ||||||
|                 ); |                 ); | ||||||
|                 $this->log->insertLog($userId ?? 0, $logMessage, 'system'); |                 $this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return $result; |             return $result; | ||||||
|         } catch (Exception $e) { |         } catch (Exception $e) { | ||||||
|             if ($userId) { |             if ($userId) { | ||||||
|                 $this->log->insertLog($userId, "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), 'system'); |                 $this->logger->log('error', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']); | ||||||
|                 Messages::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage()); |                 Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to add {$ip}: " . $e->getMessage()); | ||||||
|             } |             } | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  | @ -287,6 +361,7 @@ class RateLimiter { | ||||||
| 
 | 
 | ||||||
|             // Remove the IP
 |             // Remove the IP
 | ||||||
|             $stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable} WHERE ip_address = ?"); |             $stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable} WHERE ip_address = ?"); | ||||||
|  | 
 | ||||||
|             $result = $stmt->execute([$ip]); |             $result = $stmt->execute([$ip]); | ||||||
| 
 | 
 | ||||||
|             if ($result && $ipDetails) { |             if ($result && $ipDetails) { | ||||||
|  | @ -298,14 +373,14 @@ class RateLimiter { | ||||||
|                     $ipDetails['created_by'], |                     $ipDetails['created_by'], | ||||||
|                     $ipDetails['reason'] |                     $ipDetails['reason'] | ||||||
|                 ); |                 ); | ||||||
|                 $this->log->insertLog($userId ?? 0, $logMessage, 'system'); |                 $this->logger->log('info', $logMessage, ['user_id' => $userId ?? null, 'scope' => 'system']); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return $result; |             return $result; | ||||||
|         } catch (Exception $e) { |         } catch (Exception $e) { | ||||||
|             if ($userId) { |             if ($userId) { | ||||||
|                 $this->log->insertLog($userId, "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), 'system'); |                 $this->logger->log('error', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage(), ['user_id' => $userId, 'scope' => 'system']); | ||||||
|                 Messages::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage()); |                 Feedback::flash('ERROR', 'DEFAULT', "IP Blacklist: Failed to remove {$ip}: " . $e->getMessage()); | ||||||
|             } |             } | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  | @ -329,18 +404,18 @@ class RateLimiter { | ||||||
|         try { |         try { | ||||||
|             // Remove expired blacklist entries
 |             // Remove expired blacklist entries
 | ||||||
|             $stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable} |             $stmt = $this->db->prepare("DELETE FROM {$this->blacklistTable} | ||||||
|                 WHERE expiry_time IS NOT NULL AND expiry_time < datetime('now')");
 |                 WHERE expiry_time IS NOT NULL AND expiry_time < NOW()");
 | ||||||
|             $stmt->execute(); |             $stmt->execute(); | ||||||
| 
 | 
 | ||||||
|             // Clean old login attempts
 |             // Clean old login attempts
 | ||||||
|             $stmt = $this->db->prepare("DELETE FROM {$this->ratelimitTable} |             $stmt = $this->db->prepare("DELETE FROM {$this->authRatelimitTable} | ||||||
|                 WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')");
 |                 WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)");
 | ||||||
|             $stmt->execute([':minutes' => $this->decayMinutes]); |             $stmt->execute([':minutes' => $this->decayMinutes]); | ||||||
| 
 | 
 | ||||||
|             return true; |             return true; | ||||||
|         } catch (Exception $e) { |         } catch (Exception $e) { | ||||||
|             $this->log->insertLog(0, "Failed to cleanup expired entries: " . $e->getMessage(), 'system'); |             $this->logger->log('error', "Failed to cleanup expired entries: " . $e->getMessage(), ['user_id' => $userId ?? null, 'scope' => 'system']); | ||||||
|             Messages::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage()); |             Feedback::flash('ERROR', 'DEFAULT', "Failed to cleanup expired entries: " . $e->getMessage()); | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | @ -366,9 +441,9 @@ class RateLimiter { | ||||||
| 
 | 
 | ||||||
|         // Check total attempts across all usernames from this IP
 |         // Check total attempts across all usernames from this IP
 | ||||||
|         $sql = "SELECT COUNT(*) as total_attempts
 |         $sql = "SELECT COUNT(*) as total_attempts
 | ||||||
|                 FROM {$this->ratelimitTable} |                 FROM {$this->authRatelimitTable} | ||||||
|                 WHERE ip_address = :ip |                 WHERE ip_address = :ip | ||||||
|                 AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
 |                 AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
 | ||||||
|         $stmt = $this->db->prepare($sql); |         $stmt = $this->db->prepare($sql); | ||||||
|         $stmt->execute([ |         $stmt->execute([ | ||||||
|             ':ip'      => $ipAddress, |             ':ip'      => $ipAddress, | ||||||
|  | @ -380,25 +455,21 @@ class RateLimiter { | ||||||
|         return $result['total_attempts'] < $this->autoBlacklistThreshold; |         return $result['total_attempts'] < $this->autoBlacklistThreshold; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function attempt($username, $ipAddress) { |     public function attempt($username, $ipAddress, $failed = true) { | ||||||
|         // Record this attempt
 |         // Only record failed attempts
 | ||||||
|         $sql = "INSERT INTO {$this->ratelimitTable} (ip_address, username) VALUES (:ip, :username)"; |         if (!$failed) { | ||||||
|         $stmt = $this->db->prepare($sql); |             return true; | ||||||
|         $stmt->execute([ |         } | ||||||
|             ':ip'           => $ipAddress, |  | ||||||
|             ':username'     => $username |  | ||||||
|         ]); |  | ||||||
| 
 | 
 | ||||||
|         // Auto-blacklist if too many attempts
 |         // Record this attempt
 | ||||||
|         if (!$this->isAllowed($username, $ipAddress)) { |         $sql = "INSERT INTO {$this->authRatelimitTable} (ip_address, username) VALUES (:ip, :username)"; | ||||||
|             $this->addToBlacklist( |         $stmt = $this->db->prepare($sql); | ||||||
|                 $ipAddress, |         try { | ||||||
|                 false, |             $stmt->execute([ | ||||||
|                 'Auto-blacklisted due to excessive login attempts', |                 ':ip'           => $ipAddress, | ||||||
|                 'system', |                 ':username'     => $username | ||||||
|                 null, |             ]); | ||||||
|                 $this->autoBlacklistDuration |         } catch (PDOException $e) { | ||||||
|             ); |  | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -407,10 +478,10 @@ class RateLimiter { | ||||||
| 
 | 
 | ||||||
|     public function tooManyAttempts($username, $ipAddress) { |     public function tooManyAttempts($username, $ipAddress) { | ||||||
|         $sql = "SELECT COUNT(*) as attempts
 |         $sql = "SELECT COUNT(*) as attempts
 | ||||||
|                 FROM {$this->ratelimitTable} |                 FROM {$this->authRatelimitTable} | ||||||
|                 WHERE ip_address = :ip |                 WHERE ip_address = :ip | ||||||
|                 AND username = :username |                 AND username = :username | ||||||
|                 AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
 |                 AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
 | ||||||
| 
 | 
 | ||||||
|         $stmt = $this->db->prepare($sql); |         $stmt = $this->db->prepare($sql); | ||||||
|         $stmt->execute([ |         $stmt->execute([ | ||||||
|  | @ -420,12 +491,33 @@ class RateLimiter { | ||||||
|         ]); |         ]); | ||||||
| 
 | 
 | ||||||
|         $result = $stmt->fetch(PDO::FETCH_ASSOC); |         $result = $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|         return $result['attempts'] >= $this->maxAttempts; | 
 | ||||||
|  |         // Also check what's in the table
 | ||||||
|  |         $sql = "SELECT * FROM {$this->authRatelimitTable} WHERE ip_address = :ip"; | ||||||
|  |         $stmt = $this->db->prepare($sql); | ||||||
|  |         $stmt->execute([':ip' => $ipAddress]); | ||||||
|  |         $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); | ||||||
|  | 
 | ||||||
|  |         $tooMany = $result['attempts'] >= $this->maxAttempts; | ||||||
|  | 
 | ||||||
|  |         // Auto-blacklist if too many attempts
 | ||||||
|  |         if ($tooMany) { | ||||||
|  |             $this->addToBlacklist( | ||||||
|  |                 $ipAddress, | ||||||
|  |                 false, | ||||||
|  |                 'Auto-blacklisted due to excessive login attempts', | ||||||
|  |                 'system', | ||||||
|  |                 null, | ||||||
|  |                 $this->autoBlacklistDuration | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $tooMany; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public function clearOldAttempts() { |     public function clearOldAttempts() { | ||||||
|         $sql = "DELETE FROM {$this->ratelimitTable} |         $sql = "DELETE FROM {$this->authRatelimitTable} | ||||||
|                 WHERE attempted_at < datetime('now', '-' || :minutes || ' minutes')";
 |                 WHERE attempted_at < DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
 | ||||||
| 
 | 
 | ||||||
|         $stmt = $this->db->prepare($sql); |         $stmt = $this->db->prepare($sql); | ||||||
|         $stmt->execute([ |         $stmt->execute([ | ||||||
|  | @ -435,10 +527,10 @@ class RateLimiter { | ||||||
| 
 | 
 | ||||||
|     public function getRemainingAttempts($username, $ipAddress) { |     public function getRemainingAttempts($username, $ipAddress) { | ||||||
|         $sql = "SELECT COUNT(*) as attempts
 |         $sql = "SELECT COUNT(*) as attempts
 | ||||||
|                 FROM {$this->ratelimitTable} |                 FROM {$this->authRatelimitTable} | ||||||
|                 WHERE ip_address = :ip |                 WHERE ip_address = :ip | ||||||
|                 AND username = :username |                 AND username = :username | ||||||
|                 AND attempted_at > datetime('now', '-' || :minutes || ' minutes')";
 |                 AND attempted_at > DATE_SUB(NOW(), INTERVAL :minutes MINUTE)";
 | ||||||
| 
 | 
 | ||||||
|         $stmt = $this->db->prepare($sql); |         $stmt = $this->db->prepare($sql); | ||||||
|         $stmt->execute([ |         $stmt->execute([ | ||||||
|  | @ -454,4 +546,123 @@ class RateLimiter { | ||||||
|     public function getDecayMinutes() { |     public function getDecayMinutes() { | ||||||
|         return $this->decayMinutes; |         return $this->decayMinutes; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if a page request is allowed | ||||||
|  |      */ | ||||||
|  |     public function isPageRequestAllowed($ipAddress, $endpoint, $userId = null) { | ||||||
|  |         // First check if IP is blacklisted
 | ||||||
|  |         if ($this->isIpBlacklisted($ipAddress)) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Then check if IP is whitelisted
 | ||||||
|  |         if ($this->isIpWhitelisted($ipAddress)) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Clean old requests
 | ||||||
|  |         $this->cleanOldPageRequests(); | ||||||
|  | 
 | ||||||
|  |         // Get limit based on endpoint type and user role
 | ||||||
|  |         $limit = $this->getPageLimitForEndpoint($endpoint, $userId); | ||||||
|  | 
 | ||||||
|  |         // Count recent requests, including this one
 | ||||||
|  |         $sql = "SELECT COUNT(*) as request_count
 | ||||||
|  |                 FROM {$this->pagesRatelimitTable} | ||||||
|  |                 WHERE ip_address = :ip | ||||||
|  |                 AND endpoint = :endpoint | ||||||
|  |                 AND request_time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
 | ||||||
|  | 
 | ||||||
|  |         $stmt = $this->db->prepare($sql); | ||||||
|  |         $stmt->execute([ | ||||||
|  |             ':ip' => $ipAddress, | ||||||
|  |             ':endpoint' => $endpoint | ||||||
|  |         ]); | ||||||
|  | 
 | ||||||
|  |         $result = $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|  |         return $result['request_count'] < $limit; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Record a page request | ||||||
|  |      */ | ||||||
|  |     public function recordPageRequest($ipAddress, $endpoint) { | ||||||
|  |         $sql = "INSERT INTO {$this->pagesRatelimitTable} (ip_address, endpoint)
 | ||||||
|  |                 VALUES (:ip, :endpoint)";
 | ||||||
|  | 
 | ||||||
|  |         $stmt = $this->db->prepare($sql); | ||||||
|  |         return $stmt->execute([ | ||||||
|  |             ':ip' => $ipAddress, | ||||||
|  |             ':endpoint' => $endpoint | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Clean old page requests | ||||||
|  |      */ | ||||||
|  |     private function cleanOldPageRequests() { | ||||||
|  |         $sql = "DELETE FROM {$this->pagesRatelimitTable} | ||||||
|  |                 WHERE request_time < DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
 | ||||||
|  | 
 | ||||||
|  |         $stmt = $this->db->prepare($sql); | ||||||
|  |         $stmt->execute(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get page rate limit for endpoint | ||||||
|  |      */ | ||||||
|  |     private function getPageLimitForEndpoint($endpoint, $userId = null) { | ||||||
|  |         // Admin users get higher limits
 | ||||||
|  |         if ($userId) { | ||||||
|  |             // Check admin rights directly from database
 | ||||||
|  |             $stmt = $this->db->prepare('SELECT COUNT(*) FROM `user_right` ur JOIN `right` r ON ur.right_id = r.id WHERE ur.user_id = ? AND r.name = ?'); | ||||||
|  |             $stmt->execute([$userId, 'superuser']); | ||||||
|  |             if ($stmt->fetchColumn() > 0) { | ||||||
|  |                 return $this->pageLimits['admin']; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Get endpoint type from the endpoint path
 | ||||||
|  |         $endpointType = $this->getEndpointType($endpoint); | ||||||
|  | 
 | ||||||
|  |         // Return specific limit if exists, otherwise default
 | ||||||
|  |         return isset($this->pageLimits[$endpointType]) | ||||||
|  |             ? $this->pageLimits[$endpointType] | ||||||
|  |             : $this->pageLimits['default']; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get endpoint type from path | ||||||
|  |      */ | ||||||
|  |     private function getEndpointType($endpoint) { | ||||||
|  |         if (strpos($endpoint, 'message') !== false) return 'message'; | ||||||
|  |         if (strpos($endpoint, 'contact') !== false) return 'contact'; | ||||||
|  |         if (strpos($endpoint, 'call') !== false) return 'call'; | ||||||
|  |         if (strpos($endpoint, 'register') !== false) return 'register'; | ||||||
|  |         if (strpos($endpoint, 'config') !== false) return 'config'; | ||||||
|  |         return 'default'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get remaining page requests | ||||||
|  |      */ | ||||||
|  |     public function getRemainingPageRequests($ipAddress, $endpoint, $userId = null) { | ||||||
|  |         $limit = $this->getPageLimitForEndpoint($endpoint, $userId); | ||||||
|  | 
 | ||||||
|  |         $sql = "SELECT COUNT(*) as request_count
 | ||||||
|  |                 FROM {$this->pagesRatelimitTable} | ||||||
|  |                 WHERE ip_address = :ip | ||||||
|  |                 AND endpoint = :endpoint | ||||||
|  |                 AND request_time > DATE_SUB(NOW(), INTERVAL 1 MINUTE)";
 | ||||||
|  | 
 | ||||||
|  |         $stmt = $this->db->prepare($sql); | ||||||
|  |         $stmt->execute([ | ||||||
|  |             ':ip' => $ipAddress, | ||||||
|  |             ':endpoint' => $endpoint | ||||||
|  |         ]); | ||||||
|  | 
 | ||||||
|  |         $result = $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|  |         return max(0, $limit - $result['request_count']); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -83,5 +83,3 @@ class Router { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -52,5 +52,3 @@ class Server { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -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' => 1, | ||||||
|  |         'cookie_samesite' => 'Strict', | ||||||
|  |         'gc_maxlifetime' => 7200 // 2 hours
 | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize session configuration | ||||||
|  |      */ | ||||||
|  |     private static function initialize() { | ||||||
|  |         if (self::$initialized) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         global $config; | ||||||
|  | 
 | ||||||
|  |         // Get session name from config or generate a random one
 | ||||||
|  |         self::$sessionName = $config['session']['name'] ?? self::generateRandomSessionName(); | ||||||
|  | 
 | ||||||
|  |         // Set session name before starting the session, 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' => 'Strict' | ||||||
|  |             ]); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Align session start options dynamically with current transport
 | ||||||
|  |         self::$sessionOptions['cookie_secure'] = $isSecure ? 1 : 0; | ||||||
|  |         self::$sessionOptions['cookie_samesite'] = 'Strict'; | ||||||
|  | 
 | ||||||
|  |         self::$initialized = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get session name from config or generate a random one | ||||||
|  |      */ | ||||||
|  |     private static function getSessionNameFromConfig($config) { | ||||||
|  |         if (isset($config['session']['name']) && !empty($config['session']['name'])) { | ||||||
|  |             return $config['session']['name']; | ||||||
|  |         } | ||||||
|  |         return self::generateRandomSessionName(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Start or resume a session with secure options | ||||||
|  |      */ | ||||||
|  |     public static function startSession() { | ||||||
|  |         self::initialize(); | ||||||
|  | 
 | ||||||
|  |         if (session_status() === PHP_SESSION_NONE) { | ||||||
|  |             if (!headers_sent()) { | ||||||
|  |                 session_start(self::$sessionOptions); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Destroy current session and clean up | ||||||
|  |      */ | ||||||
|  |     public static function destroySession() { | ||||||
|  |         if (session_status() === PHP_SESSION_ACTIVE) { | ||||||
|  |             session_unset(); | ||||||
|  |             session_destroy(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get current username if set | ||||||
|  |      */ | ||||||
|  |     public static function getUsername() { | ||||||
|  |         return isset($_SESSION['username']) ? htmlspecialchars($_SESSION['username']) : null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get current user ID if set | ||||||
|  |      */ | ||||||
|  |     public static function getUserId() { | ||||||
|  |         return isset($_SESSION['user_id']) ? (int)$_SESSION['user_id'] : null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if current session is valid | ||||||
|  |      * | ||||||
|  |      * @param bool $strict If true, will return false for new/unauthenticated sessions | ||||||
|  |      * @return bool True if session is valid, false otherwise | ||||||
|  |      */ | ||||||
|  |     public static function isValidSession($strict = true) { | ||||||
|  |         // 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' => 'Strict' | ||||||
|  |             ]); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Start fresh session
 | ||||||
|  |         self::startSession(); | ||||||
|  | 
 | ||||||
|  |         // Reset session timeout flag
 | ||||||
|  |         unset($_SESSION['session_timeout_shown']); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Create a new authenticated session for a user | ||||||
|  |      */ | ||||||
|  |     public static function createAuthSession($userId, $username, $rememberMe, $config) { | ||||||
|  |         // Ensure session is started
 | ||||||
|  |         self::startSession(); | ||||||
|  | 
 | ||||||
|  |         // Set session variables
 | ||||||
|  |         $_SESSION['user_id'] = $userId; | ||||||
|  |         $_SESSION['username'] = $username; | ||||||
|  |         $_SESSION['LAST_ACTIVITY'] = time(); | ||||||
|  |         $_SESSION['REMEMBER_ME'] = $rememberMe; | ||||||
|  | 
 | ||||||
|  |         // Set cookie lifetime based on remember me
 | ||||||
|  |         $cookieLifetime = $rememberMe ? time() + (30 * 24 * 60 * 60) : 0; | ||||||
|  | 
 | ||||||
|  |         // Update session cookie with remember me setting
 | ||||||
|  |         if (!headers_sent()) { | ||||||
|  |             setcookie( | ||||||
|  |                 session_name(), | ||||||
|  |                 session_id(), | ||||||
|  |                 [ | ||||||
|  |                     'expires' => $cookieLifetime, | ||||||
|  |                     'path' => $config['folder'] ?? '/', | ||||||
|  |                     'domain' => $config['domain'] ?? '', | ||||||
|  |                     'secure' => isset($_SERVER['HTTPS']), | ||||||
|  |                     'httponly' => true, | ||||||
|  |                     'samesite' => 'Strict' | ||||||
|  |                 ] | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             // Set username cookie
 | ||||||
|  |             setcookie('username', $username, [ | ||||||
|  |                 'expires' => $cookieLifetime, | ||||||
|  |                 'path' => $config['folder'] ?? '/', | ||||||
|  |                 'domain' => $config['domain'] ?? '', | ||||||
|  |                 'secure' => isset($_SERVER['HTTPS']), | ||||||
|  |                 'httponly' => true, | ||||||
|  |                 'samesite' => 'Strict' | ||||||
|  |             ]); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if ($rememberMe) { | ||||||
|  |             self::setRememberMe(true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Store 2FA pending information in session | ||||||
|  |      */ | ||||||
|  |     public static function store2FAPending($userId, $username, $rememberMe = false) { | ||||||
|  |         $_SESSION['2fa_pending_user_id'] = $userId; | ||||||
|  |         $_SESSION['2fa_pending_username'] = $username; | ||||||
|  |         if ($rememberMe) { | ||||||
|  |             $_SESSION['2fa_pending_remember'] = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Clear 2FA pending information from session | ||||||
|  |      */ | ||||||
|  |     public static function clear2FAPending() { | ||||||
|  |         unset($_SESSION['2fa_pending_user_id']); | ||||||
|  |         unset($_SESSION['2fa_pending_username']); | ||||||
|  |         unset($_SESSION['2fa_pending_remember']); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get 2FA pending information | ||||||
|  |      */ | ||||||
|  |     public static function get2FAPending() { | ||||||
|  |         if (!isset($_SESSION['2fa_pending_user_id']) || !isset($_SESSION['2fa_pending_username'])) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return [ | ||||||
|  |             'user_id' => $_SESSION['2fa_pending_user_id'], | ||||||
|  |             'username' => $_SESSION['2fa_pending_username'], | ||||||
|  |             'remember_me' => isset($_SESSION['2fa_pending_remember']) | ||||||
|  |         ]; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -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; | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,420 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Class TwoFactorAuthentication | ||||||
|  |  * | ||||||
|  |  * Handles two-factor authentication functionality using TOTP (Time-based One-Time Password). | ||||||
|  |  * Internal implementation without external dependencies. | ||||||
|  |  */ | ||||||
|  | class TwoFactorAuthentication { | ||||||
|  |     private $db; | ||||||
|  |     private $secretLength = 20; // 160 bits for SHA1
 | ||||||
|  |     private $period = 30;       // Time step in seconds (T0)
 | ||||||
|  |     private $digits = 6;        // Number of digits in TOTP code
 | ||||||
|  |     private $algorithm = 'sha1'; // HMAC algorithm
 | ||||||
|  |     private $issuer = 'TotalMeet'; | ||||||
|  |     private $window = 1;        // Time window of 1 step before/after
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Constructor | ||||||
|  |      * | ||||||
|  |      * @param PDO $database Database connection | ||||||
|  |      */ | ||||||
|  |     public function __construct($database) { | ||||||
|  |         if ($database instanceof PDO) { | ||||||
|  |             $this->db = $database; | ||||||
|  |         } else { | ||||||
|  |             $this->db = $database->getConnection(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Enable 2FA for a user | ||||||
|  |      * | ||||||
|  |      * @param int $userId User ID | ||||||
|  |      * @param string $secret Secret key (base32 encoded) | ||||||
|  |      * @param string $code Verification code | ||||||
|  |      * @return bool True if enabled successfully | ||||||
|  |      */ | ||||||
|  |     public function enable($userId, $secret = null, $code = null) { | ||||||
|  |         try { | ||||||
|  |             // Check if 2FA is already enabled
 | ||||||
|  |             $stmt = $this->db->prepare('SELECT enabled FROM user_2fa WHERE user_id = ?'); | ||||||
|  |             $stmt->execute([$userId]); | ||||||
|  |             $existing = $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|  | 
 | ||||||
|  |             if ($existing && $existing['enabled']) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // If no secret provided, generate one and return setup data
 | ||||||
|  |             if ($secret === null) { | ||||||
|  |                 // Generate secret key
 | ||||||
|  |                 $secret = $this->generateSecret(); | ||||||
|  | 
 | ||||||
|  |                 // Get user's username for the QR code
 | ||||||
|  |                 $stmt = $this->db->prepare('SELECT username FROM user WHERE id = ?'); | ||||||
|  |                 $stmt->execute([$userId]); | ||||||
|  |                 $user = $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|  | 
 | ||||||
|  |                 // Generate backup codes
 | ||||||
|  |                 $backupCodes = $this->generateBackupCodes(); | ||||||
|  | 
 | ||||||
|  |                 // Store in database without enabling yet
 | ||||||
|  |                 $this->db->beginTransaction(); | ||||||
|  | 
 | ||||||
|  |                 $stmt = $this->db->prepare(' | ||||||
|  |                     INSERT INTO user_2fa (user_id, secret_key, backup_codes, enabled, created_at) | ||||||
|  |                     VALUES (?, ?, ?, 0, NOW()) | ||||||
|  |                     ON DUPLICATE KEY UPDATE | ||||||
|  |                         secret_key = VALUES(secret_key), | ||||||
|  |                         backup_codes = VALUES(backup_codes), | ||||||
|  |                         enabled = VALUES(enabled), | ||||||
|  |                         created_at = VALUES(created_at) | ||||||
|  |                 '); | ||||||
|  | 
 | ||||||
|  |                 $stmt->execute([ | ||||||
|  |                     $userId, | ||||||
|  |                     $secret, | ||||||
|  |                     json_encode($backupCodes) | ||||||
|  |                 ]); | ||||||
|  | 
 | ||||||
|  |                 $this->db->commit(); | ||||||
|  | 
 | ||||||
|  |                 // Generate otpauth URL for QR code
 | ||||||
|  |                 $otpauthUrl = $this->generateOtpauthUrl($user['username'], $secret); | ||||||
|  | 
 | ||||||
|  |                 return [ | ||||||
|  |                     'success' => true, | ||||||
|  |                     'data' => [ | ||||||
|  |                         'secret' => $secret, | ||||||
|  |                         'otpauthUrl' => $otpauthUrl, | ||||||
|  |                         'backupCodes' => $backupCodes | ||||||
|  |                     ] | ||||||
|  |                 ]; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // If secret and code provided, verify the code and enable 2FA
 | ||||||
|  |             if ($code !== null) { | ||||||
|  |                 // Verify the setup code
 | ||||||
|  |                 if (!$this->verify($userId, $code)) { | ||||||
|  |                     error_log("Code verification failed"); | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Enable 2FA
 | ||||||
|  |                 $stmt = $this->db->prepare(' | ||||||
|  |                     UPDATE user_2fa | ||||||
|  |                     SET enabled = 1 | ||||||
|  |                     WHERE user_id = ? AND secret_key = ? | ||||||
|  |                 '); | ||||||
|  |                 return $stmt->execute([$userId, $secret]); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return false; | ||||||
|  | 
 | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             if ($this->db->inTransaction()) { | ||||||
|  |                 $this->db->rollBack(); | ||||||
|  |             } | ||||||
|  |             error_log('2FA enable error: ' . $e->getMessage()); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Verify a 2FA code | ||||||
|  |      * | ||||||
|  |      * @param int $userId User ID | ||||||
|  |      * @param string $code The verification code | ||||||
|  |      * @return bool True if verified, false otherwise | ||||||
|  |      */ | ||||||
|  |     public function verify($userId, $code) { | ||||||
|  |         try { | ||||||
|  |             // Get user's 2FA settings
 | ||||||
|  |             $settings = $this->getUserSettings($userId); | ||||||
|  |             if (!$settings) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Check if code matches a backup code
 | ||||||
|  |             if ($this->verifyBackupCode($userId, $code)) { | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Get current Unix timestamp
 | ||||||
|  |             $currentTime = time(); | ||||||
|  | 
 | ||||||
|  |             // Check time window
 | ||||||
|  |             for ($timeSlot = -$this->window; $timeSlot <= $this->window; $timeSlot++) { | ||||||
|  |                 $checkTime = $currentTime + ($timeSlot * $this->period); | ||||||
|  |                 $generatedCode = $this->generateCode($settings['secret_key'], $checkTime); | ||||||
|  |                 if (hash_equals($generatedCode, $code)) { | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return false; | ||||||
|  | 
 | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             error_log('2FA verification error: ' . $e->getMessage()); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Generate a random secret key | ||||||
|  |      * | ||||||
|  |      * @return string Base32 encoded secret | ||||||
|  |      */ | ||||||
|  |     private function generateSecret() { | ||||||
|  |         // Generate random bytes (160 bits for SHA1)
 | ||||||
|  |         $random = random_bytes($this->secretLength); | ||||||
|  |         return $this->base32Encode($random); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Base32 encode data | ||||||
|  |      * | ||||||
|  |      * @param string $data Data to encode | ||||||
|  |      * @return string Base32 encoded string | ||||||
|  |      */ | ||||||
|  |     private function base32Encode($data) { | ||||||
|  |         $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; | ||||||
|  |         $binary = ''; | ||||||
|  |         $encoded = ''; | ||||||
|  | 
 | ||||||
|  |         // Convert to binary
 | ||||||
|  |         for ($i = 0; $i < strlen($data); $i++) { | ||||||
|  |             $binary .= str_pad(decbin(ord($data[$i])), 8, '0', STR_PAD_LEFT); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Process 5 bits at a time
 | ||||||
|  |         for ($i = 0; $i < strlen($binary); $i += 5) { | ||||||
|  |             $chunk = substr($binary, $i, 5); | ||||||
|  |             if (strlen($chunk) < 5) { | ||||||
|  |                 $chunk = str_pad($chunk, 5, '0', STR_PAD_RIGHT); | ||||||
|  |             } | ||||||
|  |             $encoded .= $alphabet[bindec($chunk)]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Add padding
 | ||||||
|  |         $padding = strlen($encoded) % 8; | ||||||
|  |         if ($padding > 0) { | ||||||
|  |             $encoded .= str_repeat('=', 8 - $padding); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $encoded; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Base32 decode data | ||||||
|  |      * | ||||||
|  |      * @param string $data Base32 encoded string | ||||||
|  |      * @return string Decoded data | ||||||
|  |      */ | ||||||
|  |     private function base32Decode($data) { | ||||||
|  |         $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; | ||||||
|  | 
 | ||||||
|  |         // Remove padding and uppercase
 | ||||||
|  |         $data = rtrim(strtoupper($data), '='); | ||||||
|  | 
 | ||||||
|  |         $binary = ''; | ||||||
|  | 
 | ||||||
|  |         // Convert to binary
 | ||||||
|  |         for ($i = 0; $i < strlen($data); $i++) { | ||||||
|  |             $position = strpos($alphabet, $data[$i]); | ||||||
|  |             if ($position === false) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             $binary .= str_pad(decbin($position), 5, '0', STR_PAD_LEFT); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $decoded = ''; | ||||||
|  |         // Process 8 bits at a time
 | ||||||
|  |         for ($i = 0; $i + 7 < strlen($binary); $i += 8) { | ||||||
|  |             $chunk = substr($binary, $i, 8); | ||||||
|  |             $decoded .= chr(bindec($chunk)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $decoded; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Generate a TOTP code for a given secret and time | ||||||
|  |      * RFC 6238 compliant implementation | ||||||
|  |      */ | ||||||
|  |     private function generateCode($secret, $time) { | ||||||
|  |         // Calculate number of time steps since Unix epoch
 | ||||||
|  |         $timeStep = (int)floor($time / $this->period); | ||||||
|  | 
 | ||||||
|  |         // Pack time into 8 bytes (64-bit big-endian)
 | ||||||
|  |         $timeBin = pack('J', $timeStep); | ||||||
|  | 
 | ||||||
|  |         // Clean secret of any padding
 | ||||||
|  |         $secret = rtrim($secret, '='); | ||||||
|  | 
 | ||||||
|  |         // Get binary secret
 | ||||||
|  |         $secretBin = $this->base32Decode($secret); | ||||||
|  | 
 | ||||||
|  |         // Calculate HMAC
 | ||||||
|  |         $hash = hash_hmac($this->algorithm, $timeBin, $secretBin, true); | ||||||
|  | 
 | ||||||
|  |         // Get dynamic truncation offset
 | ||||||
|  |         $offset = ord($hash[strlen($hash) - 1]) & 0xF; | ||||||
|  | 
 | ||||||
|  |         // Generate 31-bit number
 | ||||||
|  |         $code = ( | ||||||
|  |             ((ord($hash[$offset]) & 0x7F) << 24) | | ||||||
|  |             ((ord($hash[$offset + 1]) & 0xFF) << 16) | | ||||||
|  |             ((ord($hash[$offset + 2]) & 0xFF) << 8) | | ||||||
|  |             (ord($hash[$offset + 3]) & 0xFF) | ||||||
|  |         ) % pow(10, $this->digits); | ||||||
|  | 
 | ||||||
|  |         $code = str_pad($code, $this->digits, '0', STR_PAD_LEFT); | ||||||
|  | 
 | ||||||
|  |         return $code; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Generate otpauth URL for QR codes | ||||||
|  |      * Format: otpauth://totp/ISSUER:ACCOUNT?secret=SECRET&issuer=ISSUER&algorithm=ALGORITHM&digits=DIGITS&period=PERIOD | ||||||
|  |      */ | ||||||
|  |     private function generateOtpauthUrl($username, $secret) { | ||||||
|  |         $params = [ | ||||||
|  |             'secret' => $secret, | ||||||
|  |             'issuer' => $this->issuer, | ||||||
|  |             'algorithm' => strtoupper($this->algorithm), | ||||||
|  |             'digits' => $this->digits, | ||||||
|  |             'period' => $this->period | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         return sprintf( | ||||||
|  |             'otpauth://totp/%s:%s?%s', | ||||||
|  |             rawurlencode($this->issuer), | ||||||
|  |             rawurlencode($username), | ||||||
|  |             http_build_query($params) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Generate backup codes | ||||||
|  |      * | ||||||
|  |      * @param int $count Number of backup codes to generate | ||||||
|  |      * @return array Array of backup codes | ||||||
|  |      */ | ||||||
|  |     private function generateBackupCodes($count = 8) { | ||||||
|  |         $codes = []; | ||||||
|  |         for ($i = 0; $i < $count; $i++) { | ||||||
|  |             $codes[] = bin2hex(random_bytes(4)); | ||||||
|  |         } | ||||||
|  |         return $codes; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Verify a backup code | ||||||
|  |      * | ||||||
|  |      * @param int $userId User ID | ||||||
|  |      * @param string $code The backup code to verify | ||||||
|  |      * @return bool True if verified, false otherwise | ||||||
|  |      */ | ||||||
|  |     private function verifyBackupCode($userId, $code) { | ||||||
|  |         try { | ||||||
|  |             $stmt = $this->db->prepare('SELECT backup_codes FROM user_2fa WHERE user_id = ?'); | ||||||
|  |             $stmt->execute([$userId]); | ||||||
|  |             $result = $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|  | 
 | ||||||
|  |             if (!$result) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $backupCodes = json_decode($result['backup_codes'], true); | ||||||
|  | 
 | ||||||
|  |             // Check if the code exists and hasn't been used
 | ||||||
|  |             $codeIndex = array_search($code, $backupCodes); | ||||||
|  |             if ($codeIndex !== false) { | ||||||
|  |                 // Remove the used code
 | ||||||
|  |                 unset($backupCodes[$codeIndex]); | ||||||
|  |                 $backupCodes = array_values($backupCodes); | ||||||
|  | 
 | ||||||
|  |                 // Update backup codes in database
 | ||||||
|  |                 $stmt = $this->db->prepare(' | ||||||
|  |                     UPDATE user_2fa | ||||||
|  |                     SET backup_codes = ? | ||||||
|  |                     WHERE user_id = ? | ||||||
|  |                 '); | ||||||
|  |                 $stmt->execute([json_encode($backupCodes), $userId]); | ||||||
|  | 
 | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return false; | ||||||
|  | 
 | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             error_log('Backup code verification error: ' . $e->getMessage()); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Disable 2FA for a user | ||||||
|  |      * | ||||||
|  |      * @param int $userId User ID | ||||||
|  |      * @return bool True if disabled successfully | ||||||
|  |      */ | ||||||
|  |     public function disable($userId) { | ||||||
|  |         try { | ||||||
|  |             // First check if user has 2FA settings
 | ||||||
|  |             $settings = $this->getUserSettings($userId); | ||||||
|  |             if (!$settings) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Delete the 2FA settings entirely instead of just disabling
 | ||||||
|  |             $stmt = $this->db->prepare(' | ||||||
|  |                 DELETE FROM user_2fa | ||||||
|  |                 WHERE user_id = ? | ||||||
|  |             '); | ||||||
|  |             return $stmt->execute([$userId]); | ||||||
|  | 
 | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             error_log('2FA disable error: ' . $e->getMessage()); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if 2FA is enabled for a user | ||||||
|  |      * | ||||||
|  |      * @param int $userId User ID | ||||||
|  |      * @return bool True if enabled | ||||||
|  |      */ | ||||||
|  |     public function isEnabled($userId) { | ||||||
|  |         try { | ||||||
|  |             $stmt = $this->db->prepare('SELECT enabled FROM user_2fa WHERE user_id = ?'); | ||||||
|  |             $stmt->execute([$userId]); | ||||||
|  |             $result = $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|  |             return $result && $result['enabled']; | ||||||
|  | 
 | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             error_log('2FA status check error: ' . $e->getMessage()); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function getUserSettings($userId) { | ||||||
|  |         try { | ||||||
|  |             $stmt = $this->db->prepare(' | ||||||
|  |                 SELECT secret_key, backup_codes, enabled | ||||||
|  |                 FROM user_2fa | ||||||
|  |                 WHERE user_id = ? | ||||||
|  |             '); | ||||||
|  |             $stmt->execute([$userId]); | ||||||
|  |             return $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|  | 
 | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             error_log('Failed to get user 2FA settings: ' . $e->getMessage()); | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| /** | /** | ||||||
|  * class User |  * class User | ||||||
|  * |  * | ||||||
|  * Handles user-related functionalities such as registration, login, rights management, and profile updates. |  * Handles user-related functionalities such as login, rights management, and profile updates. | ||||||
|  */ |  */ | ||||||
| class User { | class User { | ||||||
|     /** |     /** | ||||||
|  | @ -11,6 +11,12 @@ class User { | ||||||
|      */ |      */ | ||||||
|     private $db; |     private $db; | ||||||
|     private $rateLimiter; |     private $rateLimiter; | ||||||
|  |     private $twoFactorAuth; | ||||||
|  |     /** | ||||||
|  |      * Cache for database schema checks | ||||||
|  |      * @var array<string,bool> | ||||||
|  |      */ | ||||||
|  |     private static $schemaCache = []; | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * User constructor. |      * User constructor. | ||||||
|  | @ -19,64 +25,88 @@ class User { | ||||||
|      * @param object $database The database object to initialize the connection. |      * @param object $database The database object to initialize the connection. | ||||||
|      */ |      */ | ||||||
|     public function __construct($database) { |     public function __construct($database) { | ||||||
|         $this->db = $database->getConnection(); |         if ($database instanceof PDO) { | ||||||
|  |             $this->db = $database; | ||||||
|  |         } else { | ||||||
|  |             $this->db = $database->getConnection(); | ||||||
|  |         } | ||||||
|  |         require_once __DIR__ . '/ratelimiter.php'; | ||||||
|  |         require_once __DIR__ . '/twoFactorAuth.php'; | ||||||
|  | 
 | ||||||
|         $this->rateLimiter = new RateLimiter($database); |         $this->rateLimiter = new RateLimiter($database); | ||||||
|  |         $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; | ||||||
|  |         } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Registers a new user. |      * Get the user's preferred theme if stored in DB (user_meta.theme). Returns null if not set. | ||||||
|      * |      * | ||||||
|      * @param string $username The username of the new user. |      * @param int $userId | ||||||
|      * @param string $password The password for the new user. |      * @return string|null | ||||||
|      * |  | ||||||
|      * @return bool|string True if registration is successful, error message otherwise. |  | ||||||
|      */ |      */ | ||||||
|     public function register($username, $password) { |     public function getUserTheme(int $userId): ?string { | ||||||
|  |         if (!$this->columnExists('user_meta', 'theme')) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|         try { |         try { | ||||||
|             // we have two inserts, start a transaction
 |             $sql = 'SELECT theme FROM user_meta WHERE user_id = :user_id LIMIT 1'; | ||||||
|             $this->db->beginTransaction(); |             $stmt = $this->db->prepare($sql); | ||||||
| 
 |             $stmt->execute([':user_id' => $userId]); | ||||||
|             // hash the password, don't store it plain
 |             $row = $stmt->fetch(PDO::FETCH_ASSOC); | ||||||
|             $hashedPassword = password_hash($password, PASSWORD_DEFAULT); |             if (!$row) { | ||||||
| 
 |                 return null; | ||||||
|             // insert into users table
 |  | ||||||
|             $sql = 'INSERT |  | ||||||
|                         INTO users (username, password) |  | ||||||
|                         VALUES (:username, :password)'; |  | ||||||
|             $query = $this->db->prepare($sql); |  | ||||||
|             $query->bindValue(':username', $username); |  | ||||||
|             $query->bindValue(':password', $hashedPassword); |  | ||||||
| 
 |  | ||||||
|             // execute the first query
 |  | ||||||
|             if (!$query->execute()) { |  | ||||||
|                 // rollback on error
 |  | ||||||
|                 $this->db->rollBack(); |  | ||||||
|                 return false; |  | ||||||
|             } |             } | ||||||
| 
 |             $theme = $row['theme'] ?? null; | ||||||
|             // insert the last user id into users_meta table
 |             return ($theme !== null && $theme !== '') ? $theme : null; | ||||||
|             $sql2 = 'INSERT |  | ||||||
|                         INTO users_meta (user_id) |  | ||||||
|                         VALUES (:user_id)'; |  | ||||||
|             $query2 = $this->db->prepare($sql2); |  | ||||||
|             $query2->bindValue(':user_id', $this->db->lastInsertId()); |  | ||||||
| 
 |  | ||||||
|             // execute the second query
 |  | ||||||
|             if (!$query2->execute()) { |  | ||||||
|                 // rollback on error
 |  | ||||||
|                 $this->db->rollBack(); |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // if all is OK, commit the transaction
 |  | ||||||
|             $this->db->commit(); |  | ||||||
|             return true; |  | ||||||
| 
 |  | ||||||
|         } catch (Exception $e) { |         } catch (Exception $e) { | ||||||
|             // rollback on any error
 |             return null; | ||||||
|             $this->db->rollBack(); |         } | ||||||
|             return $e->getMessage(); |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * 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; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -86,15 +116,13 @@ class User { | ||||||
|      * |      * | ||||||
|      * @param string $username The username of the user. |      * @param string $username The username of the user. | ||||||
|      * @param string $password The password of the user. |      * @param string $password The password of the user. | ||||||
|  |      * @param string $twoFactorCode Optional. The 2FA code if 2FA is enabled. | ||||||
|      * |      * | ||||||
|      * @return bool True if login is successful, false otherwise. |      * @return array Login result with status and any necessary data | ||||||
|      */ |      */ | ||||||
|     public function login($username, $password) { |     public function login($username, $password, $twoFactorCode = null) { | ||||||
|         // get client IP address
 |         // Get user's IP address
 | ||||||
|         $ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; |         $ipAddress = getUserIP(); | ||||||
| 
 |  | ||||||
|         // Record attempt
 |  | ||||||
|         $this->rateLimiter->attempt($username, $ipAddress); |  | ||||||
| 
 | 
 | ||||||
|         // Check rate limiting first
 |         // Check rate limiting first
 | ||||||
|         if (!$this->rateLimiter->isAllowed($username, $ipAddress)) { |         if (!$this->rateLimiter->isAllowed($username, $ipAddress)) { | ||||||
|  | @ -103,20 +131,49 @@ class User { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Then check credentials
 |         // Then check credentials
 | ||||||
|         $query = $this->db->prepare("SELECT * FROM users WHERE username = :username"); |         $query = $this->db->prepare("SELECT * FROM user WHERE username = :username"); | ||||||
|         $query->bindParam(':username', $username); |         $query->bindParam(':username', $username); | ||||||
|         $query->execute(); |         $query->execute(); | ||||||
| 
 | 
 | ||||||
|         $user = $query->fetch(PDO::FETCH_ASSOC); |         $user = $query->fetch(PDO::FETCH_ASSOC); | ||||||
|         if ($user && password_verify($password, $user['password'])) { |         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['user_id'] = $user['id']; | ||||||
|             $_SESSION['username'] = $user['username']; |             $_SESSION['username'] = $user['username']; | ||||||
|             return true; |             $_SESSION['CREATED'] = time(); | ||||||
|  |             $_SESSION['LAST_ACTIVITY'] = time(); | ||||||
|  |             return [ | ||||||
|  |                 'status' => 'success', | ||||||
|  |                 'user_id' => $user['id'], | ||||||
|  |                 'username' => $user['username'] | ||||||
|  |             ]; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Get remaining attempts AFTER this failed attempt
 |         // Get remaining attempts AFTER this failed attempt
 | ||||||
|         $remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress); |         $remainingAttempts = $this->rateLimiter->getRemainingAttempts($username, $ipAddress); | ||||||
|         throw new Exception("Invalid credentials. {$remainingAttempts} attempts remaining."); |         return [ | ||||||
|  |             'status' => 'failed', | ||||||
|  |             'message' => "Invalid credentials. {$remainingAttempts} attempts remaining." | ||||||
|  |         ]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -129,7 +186,7 @@ class User { | ||||||
|      */ |      */ | ||||||
|     // FIXME not used now?
 |     // FIXME not used now?
 | ||||||
|     public function getUserId($username) { |     public function getUserId($username) { | ||||||
|         $sql = 'SELECT id FROM users WHERE username = :username'; |         $sql = 'SELECT id FROM user WHERE username = :username'; | ||||||
|         $query = $this->db->prepare($sql); |         $query = $this->db->prepare($sql); | ||||||
|         $query->bindParam(':username', $username); |         $query->bindParam(':username', $username); | ||||||
| 
 | 
 | ||||||
|  | @ -143,24 +200,24 @@ class User { | ||||||
|     /** |     /** | ||||||
|      * Fetches user details by user ID. |      * Fetches user details by user ID. | ||||||
|      * |      * | ||||||
|      * @param int $user_id The user ID. |      * @param int $userId The user ID. | ||||||
|      * |      * | ||||||
|      * @return array|null User details or null if not found. |      * @return array|null User details or null if not found. | ||||||
|      */ |      */ | ||||||
|     public function getUserDetails($user_id) { |     public function getUserDetails($userId) { | ||||||
|         $sql = 'SELECT |         $sql = 'SELECT | ||||||
|                     um.*, |                     um.*, | ||||||
|                     u.username |                     u.username | ||||||
|                 FROM |                 FROM | ||||||
|                     users_meta um |                     user_meta um | ||||||
|                 LEFT JOIN users u |                 LEFT JOIN user u | ||||||
|                     ON um.user_id = u.id |                     ON um.user_id = u.id | ||||||
|                 WHERE |                 WHERE | ||||||
|                     u.id = :user_id'; |                     u.id = :user_id'; | ||||||
| 
 | 
 | ||||||
|         $query = $this->db->prepare($sql); |         $query = $this->db->prepare($sql); | ||||||
|         $query->execute([ |         $query->execute([ | ||||||
|             ':user_id'		=> $user_id, |             ':user_id'		=> $userId, | ||||||
|         ]); |         ]); | ||||||
| 
 | 
 | ||||||
|         return $query->fetchAll(PDO::FETCH_ASSOC); |         return $query->fetchAll(PDO::FETCH_ASSOC); | ||||||
|  | @ -171,19 +228,19 @@ class User { | ||||||
|     /** |     /** | ||||||
|      * Grants a user a specific right. |      * Grants a user a specific right. | ||||||
|      * |      * | ||||||
|      * @param int $user_id The user ID. |      * @param int $userId    The user ID. | ||||||
|      * @param int $right_id The right ID to grant. |      * @param int $right_id  The right ID to grant. | ||||||
|      * |      * | ||||||
|      * @return void |      * @return void | ||||||
|      */ |      */ | ||||||
|     public function addUserRight($user_id, $right_id) { |     public function addUserRight($userId, $right_id) { | ||||||
|         $sql = 'INSERT INTO users_rights |         $sql = 'INSERT INTO user_right | ||||||
|                     (user_id, right_id) |                     (user_id, right_id) | ||||||
|                 VALUES |                 VALUES | ||||||
|                     (:user_id, :right_id)'; |                     (:user_id, :right_id)'; | ||||||
|         $query = $this->db->prepare($sql); |         $query = $this->db->prepare($sql); | ||||||
|         $query->execute([ |         $query->execute([ | ||||||
|             ':user_id'		=> $user_id, |             ':user_id'		=> $userId, | ||||||
|             ':right_id'		=> $right_id, |             ':right_id'		=> $right_id, | ||||||
|         ]); |         ]); | ||||||
|     } |     } | ||||||
|  | @ -192,20 +249,20 @@ class User { | ||||||
|     /** |     /** | ||||||
|      * Revokes a specific right from a user. |      * Revokes a specific right from a user. | ||||||
|      * |      * | ||||||
|      * @param int $user_id The user ID. |      * @param int $userId    The user ID. | ||||||
|      * @param int $right_id The right ID to revoke. |      * @param int $right_id  The right ID to revoke. | ||||||
|      * |      * | ||||||
|      * @return void |      * @return void | ||||||
|      */ |      */ | ||||||
|     public function removeUserRight($user_id, $right_id) { |     public function removeUserRight($userId, $right_id) { | ||||||
|         $sql = 'DELETE FROM users_rights |         $sql = 'DELETE FROM user_right | ||||||
|                 WHERE |                 WHERE | ||||||
|                     user_id = :user_id |                     user_id = :user_id | ||||||
|                 AND |                 AND | ||||||
|                     right_id = :right_id'; |                     right_id = :right_id'; | ||||||
|         $query = $this->db->prepare($sql); |         $query = $this->db->prepare($sql); | ||||||
|         $query->execute([ |         $query->execute([ | ||||||
|             ':user_id'		=> $user_id, |             ':user_id'		=> $userId, | ||||||
|             ':right_id'		=> $right_id, |             ':right_id'		=> $right_id, | ||||||
|         ]); |         ]); | ||||||
|     } |     } | ||||||
|  | @ -220,7 +277,7 @@ class User { | ||||||
|         $sql = 'SELECT |         $sql = 'SELECT | ||||||
|                     id AS right_id, |                     id AS right_id, | ||||||
|                     name AS right_name |                     name AS right_name | ||||||
|                 FROM rights |                 FROM `right` | ||||||
|                 ORDER BY id ASC'; |                 ORDER BY id ASC'; | ||||||
|         $query = $this->db->prepare($sql); |         $query = $this->db->prepare($sql); | ||||||
|         $query->execute(); |         $query->execute(); | ||||||
|  | @ -233,27 +290,27 @@ class User { | ||||||
|     /** |     /** | ||||||
|      * Retrieves the rights assigned to a specific user. |      * Retrieves the rights assigned to a specific user. | ||||||
|      * |      * | ||||||
|      * @param int $user_id The user ID. |      * @param int $userId The user ID. | ||||||
|      * |      * | ||||||
|      * @return array List of user rights. |      * @return array List of user rights. | ||||||
|      */ |      */ | ||||||
|     public function getUserRights($user_id) { |     public function getUserRights($userId) { | ||||||
|         $sql = 'SELECT |         $sql = 'SELECT | ||||||
|                     u.id AS user_id, |                     u.id AS user_id, | ||||||
|                     r.id AS right_id, |                     r.id AS right_id, | ||||||
|                     r.name AS right_name |                     r.name AS right_name | ||||||
|                 FROM |                 FROM | ||||||
|                     users u |                     `user` u | ||||||
|                     LEFT JOIN users_rights ur |                     LEFT JOIN `user_right` ur | ||||||
|                         ON u.id = ur.user_id |                         ON u.id = ur.user_id | ||||||
|                     LEFT JOIN rights r |                     LEFT JOIN `right` r | ||||||
|                         ON ur.right_id = r.id |                         ON ur.right_id = r.id | ||||||
|                 WHERE |                 WHERE | ||||||
|                     u.id = :user_id'; |                     u.id = :user_id'; | ||||||
| 
 | 
 | ||||||
|         $query = $this->db->prepare($sql); |         $query = $this->db->prepare($sql); | ||||||
|         $query->execute([ |         $query->execute([ | ||||||
|             ':user_id'		=> $user_id, |             ':user_id'		=> $userId, | ||||||
|         ]); |         ]); | ||||||
| 
 | 
 | ||||||
|         $result = $query->fetchAll(PDO::FETCH_ASSOC); |         $result = $query->fetchAll(PDO::FETCH_ASSOC); | ||||||
|  | @ -262,7 +319,7 @@ class User { | ||||||
|         $specialEntries = []; |         $specialEntries = []; | ||||||
| 
 | 
 | ||||||
|         // user 1 is always superuser
 |         // user 1 is always superuser
 | ||||||
|         if ($user_id == 1) { |         if ($userId == 1) { | ||||||
|             $specialEntries = [ |             $specialEntries = [ | ||||||
|                 [ |                 [ | ||||||
|                     'user_id' => 1, |                     'user_id' => 1, | ||||||
|  | @ -272,7 +329,7 @@ class User { | ||||||
|             ]; |             ]; | ||||||
| 
 | 
 | ||||||
|         // user 2 is always demo
 |         // user 2 is always demo
 | ||||||
|         } elseif ($user_id == 2) { |         } elseif ($userId == 2) { | ||||||
|             $specialEntries = [ |             $specialEntries = [ | ||||||
|                 [ |                 [ | ||||||
|                     'user_id' => 2, |                     'user_id' => 2, | ||||||
|  | @ -296,17 +353,17 @@ class User { | ||||||
|     /** |     /** | ||||||
|      * Check if the user has a specific right. |      * Check if the user has a specific right. | ||||||
|      * |      * | ||||||
|      * @param int $user_id The user ID. |      * @param int    $userId      The user ID. | ||||||
|      * @param string $right_name The human-readable name of the user right. |      * @param string $right_name  The human-readable name of the user right. | ||||||
|      * |      * | ||||||
|      * @return bool True if the user has the right, false otherwise. |      * @return bool True if the user has the right, false otherwise. | ||||||
|      */ |      */ | ||||||
|     function hasRight($user_id, $right_name) { |     function hasRight($userId, $right_name) { | ||||||
|         $userRights = $this->getUserRights($user_id); |         $userRights = $this->getUserRights($userId); | ||||||
|         $userHasRight = false; |         $userHasRight = false; | ||||||
| 
 | 
 | ||||||
|         // superuser always has all the rights
 |         // superuser always has all the rights
 | ||||||
|         if ($user_id === 1) { |         if ($userId === 1) { | ||||||
|             $userHasRight = true; |             $userHasRight = true; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -325,8 +382,8 @@ class User { | ||||||
|     /** |     /** | ||||||
|      * Updates a user's metadata in the database. |      * Updates a user's metadata in the database. | ||||||
|      * |      * | ||||||
|      * @param int $user_id The ID of the user to update. |      * @param int   $userId       The ID of the user to update. | ||||||
|      * @param array $updatedUser An associative array containing updated user data: |      * @param array $updatedUser  An associative array containing updated user data: | ||||||
|      *  - 'name' (string): The updated name of the user. |      *  - 'name' (string): The updated name of the user. | ||||||
|      *  - 'email' (string): The updated email of the user. |      *  - 'email' (string): The updated email of the user. | ||||||
|      *  - 'timezone' (string): The updated timezone of the user. |      *  - 'timezone' (string): The updated timezone of the user. | ||||||
|  | @ -334,9 +391,9 @@ class User { | ||||||
|      * |      * | ||||||
|      * @return bool|string Returns true if the update is successful, or an error message if an exception occurs. |      * @return bool|string Returns true if the update is successful, or an error message if an exception occurs. | ||||||
|      */ |      */ | ||||||
|     public function editUser($user_id, $updatedUser) { |     public function editUser($userId, $updatedUser) { | ||||||
|         try { |         try { | ||||||
|             $sql = 'UPDATE users_meta SET |             $sql = 'UPDATE user_meta SET | ||||||
|                         name = :name, |                         name = :name, | ||||||
|                         email = :email, |                         email = :email, | ||||||
|                         timezone = :timezone, |                         timezone = :timezone, | ||||||
|  | @ -344,7 +401,7 @@ class User { | ||||||
|                     WHERE user_id = :user_id'; |                     WHERE user_id = :user_id'; | ||||||
|             $query = $this->db->prepare($sql); |             $query = $this->db->prepare($sql); | ||||||
|             $query->execute([ |             $query->execute([ | ||||||
|                 ':user_id'	=> $user_id, |                 ':user_id'	=> $userId, | ||||||
|                 ':name'		=> $updatedUser['name'], |                 ':name'		=> $updatedUser['name'], | ||||||
|                 ':email'	=> $updatedUser['email'], |                 ':email'	=> $updatedUser['email'], | ||||||
|                 ':timezone'	=> $updatedUser['timezone'], |                 ':timezone'	=> $updatedUser['timezone'], | ||||||
|  | @ -363,20 +420,20 @@ class User { | ||||||
|     /** |     /** | ||||||
|      * Removes a user's avatar from the database and deletes the associated file. |      * Removes a user's avatar from the database and deletes the associated file. | ||||||
|      * |      * | ||||||
|      * @param int $user_id The ID of the user whose avatar is being removed. |      * @param 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. |      * @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. |      * @return bool|string Returns true if the avatar is successfully removed, or an error message if an exception occurs. | ||||||
|      */ |      */ | ||||||
|     public function removeAvatar($user_id, $old_avatar = '') { |     public function removeAvatar($userId, $old_avatar = '') { | ||||||
|         try { |         try { | ||||||
|             // remove from database
 |             // remove from database
 | ||||||
|             $sql = 'UPDATE users_meta SET |             $sql = 'UPDATE user_meta SET | ||||||
|                         avatar = NULL |                         avatar = NULL | ||||||
|                     WHERE user_id = :user_id'; |                     WHERE user_id = :user_id'; | ||||||
|             $query = $this->db->prepare($sql); |             $query = $this->db->prepare($sql); | ||||||
|             $query->execute([ |             $query->execute([ | ||||||
|                 ':user_id'	=> $user_id, |                 ':user_id'	=> $userId, | ||||||
|             ]); |             ]); | ||||||
| 
 | 
 | ||||||
|             // delete the old avatar file
 |             // delete the old avatar file
 | ||||||
|  | @ -396,14 +453,14 @@ class User { | ||||||
|     /** |     /** | ||||||
|      * Updates a user's avatar by uploading a new file and saving its path in the database. |      * Updates a user's avatar by uploading a new file and saving its path in the database. | ||||||
|      * |      * | ||||||
|      * @param int $user_id The ID of the user whose avatar is being updated. |      * @param int    $userId        The ID of the user whose avatar is being updated. | ||||||
|      * @param array $avatar_file The uploaded avatar file from the $_FILES array. |      * @param array  $avatar_file   The uploaded avatar file from the $_FILES array. | ||||||
|      *                           Should include 'tmp_name', 'name', 'error', etc. |      *                              Should include 'tmp_name', 'name', 'error', etc. | ||||||
|      * @param string $avatars_path The directory path where avatar files should be saved. |      * @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. |      * @return bool|string Returns true if the avatar is successfully updated, or an error message if an exception occurs. | ||||||
|      */ |      */ | ||||||
|     public function changeAvatar($user_id, $avatar_file, $avatars_path) { |     public function changeAvatar($userId, $avatar_file, $avatars_path) { | ||||||
|         try { |         try { | ||||||
|             // check if the file was uploaded
 |             // check if the file was uploaded
 | ||||||
|             if (isset($avatar_file) && $avatar_file['error'] === UPLOAD_ERR_OK) { |             if (isset($avatar_file) && $avatar_file['error'] === UPLOAD_ERR_OK) { | ||||||
|  | @ -420,13 +477,13 @@ class User { | ||||||
|                     if (move_uploaded_file($fileTmpPath, $dest_path)) { |                     if (move_uploaded_file($fileTmpPath, $dest_path)) { | ||||||
|                         try { |                         try { | ||||||
|                             // update user's avatar path in DB
 |                             // update user's avatar path in DB
 | ||||||
|                             $sql = 'UPDATE users_meta SET |                             $sql = 'UPDATE user_meta SET | ||||||
|                                         avatar = :avatar |                                         avatar = :avatar | ||||||
|                                     WHERE user_id = :user_id'; |                                     WHERE user_id = :user_id'; | ||||||
|                             $query = $this->db->prepare($sql); |                             $query = $this->db->prepare($sql); | ||||||
|                             $query->execute([ |                             $query->execute([ | ||||||
|                                 ':avatar' => $newFileName, |                                 ':avatar' => $newFileName, | ||||||
|                                 ':user_id' => $user_id |                                 ':user_id' => $userId | ||||||
|                             ]); |                             ]); | ||||||
|                             // all went OK
 |                             // all went OK
 | ||||||
|                             $_SESSION['notice'] .= 'Avatar updated successfully. '; |                             $_SESSION['notice'] .= 'Avatar updated successfully. '; | ||||||
|  | @ -449,6 +506,99 @@ class User { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } |     /** | ||||||
|  |      * Get all users for messaging | ||||||
|  |      * | ||||||
|  |      * @return array List of users with their IDs and usernames | ||||||
|  |      */ | ||||||
|  |     public function getUsers() { | ||||||
|  |         $sql = "SELECT id, username
 | ||||||
|  |                 FROM `user` | ||||||
|  |                 ORDER BY username ASC";
 | ||||||
| 
 | 
 | ||||||
| ?>
 |         $stmt = $this->db->prepare($sql); | ||||||
|  |         $stmt->execute(); | ||||||
|  | 
 | ||||||
|  |         return $stmt->fetchAll(PDO::FETCH_ASSOC); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Enable two-factor authentication for a user | ||||||
|  |      * | ||||||
|  |      * @param int    $userId  User ID | ||||||
|  |      * @param string $secret  Secret key to use | ||||||
|  |      * @param string $code    Verification code to validate | ||||||
|  |      * @return bool True if enabled successfully | ||||||
|  |      */ | ||||||
|  |     public function enableTwoFactor($userId, $secret = null, $code = null) { | ||||||
|  |         return $this->twoFactorAuth->enable($userId, $secret, $code); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Disable two-factor authentication for a user | ||||||
|  |      * | ||||||
|  |      * @param int $userId User ID | ||||||
|  |      * @return bool True if disabled successfully | ||||||
|  |      */ | ||||||
|  |     public function disableTwoFactor($userId) { | ||||||
|  |         return $this->twoFactorAuth->disable($userId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Verify a two-factor authentication code | ||||||
|  |      * | ||||||
|  |      * @param int    $userId  User ID | ||||||
|  |      * @param string $code    The verification code | ||||||
|  |      * @return bool True if verified | ||||||
|  |      */ | ||||||
|  |     public function verifyTwoFactor($userId, $code) { | ||||||
|  |         return $this->twoFactorAuth->verify($userId, $code); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if two-factor authentication is enabled for a user | ||||||
|  |      * | ||||||
|  |      * @param int $userId User ID | ||||||
|  |      * @return bool True if enabled | ||||||
|  |      */ | ||||||
|  |     public function isTwoFactorEnabled($userId) { | ||||||
|  |         return $this->twoFactorAuth->isEnabled($userId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Change a user's password | ||||||
|  |      * | ||||||
|  |      * @param int    $userId           User ID | ||||||
|  |      * @param string $currentPassword  Current password for verification | ||||||
|  |      * @param string $newPassword      New password to set | ||||||
|  |      * @return bool True if password was changed successfully | ||||||
|  |      */ | ||||||
|  |     public function changePassword($userId, $currentPassword, $newPassword) { | ||||||
|  |         try { | ||||||
|  |             // First verify the current password
 | ||||||
|  |             $sql = "SELECT password FROM user WHERE id = :user_id"; | ||||||
|  |             $query = $this->db->prepare($sql); | ||||||
|  |             $query->execute([':user_id' => $userId]); | ||||||
|  |             $user = $query->fetch(PDO::FETCH_ASSOC); | ||||||
|  | 
 | ||||||
|  |             if (!$user || !password_verify($currentPassword, $user['password'])) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Hash the new password
 | ||||||
|  |             $hashedPassword = password_hash($newPassword, PASSWORD_DEFAULT); | ||||||
|  | 
 | ||||||
|  |             // Update the password
 | ||||||
|  |             $sql = "UPDATE user SET password = :password WHERE id = :user_id"; | ||||||
|  |             $query = $this->db->prepare($sql); | ||||||
|  |             return $query->execute([ | ||||||
|  |                 ':password' => $hashedPassword, | ||||||
|  |                 ':user_id' => $userId | ||||||
|  |             ]); | ||||||
|  | 
 | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             error_log("Error changing password: " . $e->getMessage()); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,110 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | class Validator { | ||||||
|  |     private $errors = []; | ||||||
|  |     private $data = []; | ||||||
|  | 
 | ||||||
|  |     public function __construct(array $data) { | ||||||
|  |         $this->data = $data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function validate(array $rules) { | ||||||
|  |         foreach ($rules as $field => $fieldRules) { | ||||||
|  |             foreach ($fieldRules as $rule => $parameter) { | ||||||
|  |                 $this->applyRule($field, $rule, $parameter); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return empty($this->errors); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function applyRule($field, $rule, $parameter) { | ||||||
|  |         $value = $this->data[$field] ?? null; | ||||||
|  | 
 | ||||||
|  |         switch ($rule) { | ||||||
|  |             case 'required': | ||||||
|  |                 if ($parameter && empty($value)) { | ||||||
|  |                     $this->addError($field, "Field is required"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 'email': | ||||||
|  |                 if (!empty($value) && !filter_var($value, FILTER_VALIDATE_EMAIL)) { | ||||||
|  |                     $this->addError($field, "Invalid email format"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 'min': | ||||||
|  |                 if (!empty($value) && strlen($value) < $parameter) { | ||||||
|  |                     $this->addError($field, "Minimum length is $parameter characters"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 'max': | ||||||
|  |                 if (!empty($value) && strlen($value) > $parameter) { | ||||||
|  |                     $this->addError($field, "Maximum length is $parameter characters"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 'numeric': | ||||||
|  |                 if (!empty($value) && !is_numeric($value)) { | ||||||
|  |                     $this->addError($field, "Must be a number"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 'phone': | ||||||
|  |                 if (!empty($value) && !preg_match('/^[+]?[\d\s-()]{7,}$/', $value)) { | ||||||
|  |                     $this->addError($field, "Invalid phone number format"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 'url': | ||||||
|  |                 if (!empty($value) && !filter_var($value, FILTER_VALIDATE_URL)) { | ||||||
|  |                     $this->addError($field, "Invalid URL format"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 'date': | ||||||
|  |                 if (!empty($value)) { | ||||||
|  |                     $date = date_parse($value); | ||||||
|  |                     if ($date['error_count'] > 0) { | ||||||
|  |                         $this->addError($field, "Invalid date format"); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 'in': | ||||||
|  |                 if (!empty($value) && !in_array($value, $parameter)) { | ||||||
|  |                     $this->addError($field, "Invalid option selected"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 'matches': | ||||||
|  |                 if ($value !== ($this->data[$parameter] ?? null)) { | ||||||
|  |                     $this->addError($field, "Does not match $parameter field"); | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |             case 'ip': | ||||||
|  |                 if (!empty($value)) { | ||||||
|  |                     // Support both IPv4 and IPv6
 | ||||||
|  |                     if (!filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) { | ||||||
|  |                         $this->addError($field, "Invalid IP address format"); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function addError($field, $message) { | ||||||
|  |         if (!isset($this->errors[$field])) { | ||||||
|  |             $this->errors[$field] = []; | ||||||
|  |         } | ||||||
|  |         $this->errors[$field][] = $message; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getErrors() { | ||||||
|  |         return $this->errors; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function hasErrors() { | ||||||
|  |         return !empty($this->errors); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function getFirstError() { | ||||||
|  |         if (!$this->hasErrors()) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         $firstField = array_key_first($this->errors); | ||||||
|  |         return $this->errors[$firstField][0]; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -10,6 +10,18 @@ return [ | ||||||
|     'domain'			=> 'localhost', |     'domain'			=> 'localhost', | ||||||
|     // subfolder for the web app, if any
 |     // subfolder for the web app, if any
 | ||||||
|     'folder'			=> '/jilo-web/', |     'folder'			=> '/jilo-web/', | ||||||
|  |     // site name used in emails and in the interface
 | ||||||
|  |     'site_name'			=> 'Jilo Web', | ||||||
|  | 
 | ||||||
|  |     // session configuration
 | ||||||
|  |     'session' => [ | ||||||
|  |         // session name, if empty a random one will be generated
 | ||||||
|  |         'name' => 'jilo', | ||||||
|  |         // 2 hours (7200) default, when "remember me" is not checked
 | ||||||
|  |         'lifetime' => '7200', | ||||||
|  |         // 30 days (2592000) default, when "remember me" is checked
 | ||||||
|  |         'remember_me_lifetime' => '2592000', | ||||||
|  |     ], | ||||||
|     // set to false to disable new registrations
 |     // set to false to disable new registrations
 | ||||||
|     'registration_enabled'	=> true, |     'registration_enabled'	=> true, | ||||||
|     // will be displayed on login screen
 |     // will be displayed on login screen
 | ||||||
|  | @ -20,21 +32,27 @@ return [ | ||||||
|     //*******************************************
 |     //*******************************************
 | ||||||
| 
 | 
 | ||||||
|     // database
 |     // database
 | ||||||
|     'db' => [ |     'db_type'			=> 'mariadb', | ||||||
|         // DB type for the web app, currently only "sqlite" is used
 | 
 | ||||||
|         'db_type'		=> 'sqlite', |     'sqlite' => [ | ||||||
|         // default is ../app/jilo-web.db
 |  | ||||||
|         'sqlite_file'		=> '../app/jilo-web.db', |         'sqlite_file'		=> '../app/jilo-web.db', | ||||||
|     ], |     ], | ||||||
|  | 
 | ||||||
|  |     'sql' => [ | ||||||
|  |         'sql_host'		=> 'localhost', | ||||||
|  |         'sql_port'		=> '3306', | ||||||
|  |         'sql_database'		=> 'jilo', | ||||||
|  |         'sql_username'		=> 'jilouser', | ||||||
|  |         'sql_password'		=> 'jilopass', | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|     // avatars path
 |     // avatars path
 | ||||||
|     'avatars_path'		=> 'uploads/avatars/', |     'avatars_path'		=> 'uploads/avatars/', | ||||||
|     // default avatar
 |     // default avatar
 | ||||||
|     'default_avatar'		=> 'static/default_avatar.png', |     'default_avatar'		=> 'static/default_avatar.png', | ||||||
|     // system info
 |     // system info
 | ||||||
|     'version'			=> '0.3', |     'version'			=> '0.4', | ||||||
|     // development has verbose error messages, production has not
 |     // development has verbose error messages, production has not
 | ||||||
|     'environment'		=> 'development', |     'environment'		=> 'development', | ||||||
| 
 | 
 | ||||||
| ]; | ]; | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,42 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Theme Configuration | ||||||
|  |  * | ||||||
|  |  * This file is auto-generated. Do not edit it manually. | ||||||
|  |  * Use the theme management interface to modify theme settings. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | return [ | ||||||
|  |     // Active theme (can be overridden by user preference)
 | ||||||
|  |     'active_theme' => 'default', | ||||||
|  | 
 | ||||||
|  |     // Available themes with their display names
 | ||||||
|  |     'available_themes' => [ | ||||||
|  |         'default' => 'Default built-in theme', | ||||||
|  |         'modern' => 'Modern theme', | ||||||
|  |         'retro' => 'Alternative retro theme' | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     // Path configurations
 | ||||||
|  |     'paths' => [ | ||||||
|  |         // Base directory for all external themes
 | ||||||
|  |         'themes' => __DIR__ . '/../../themes', | ||||||
|  | 
 | ||||||
|  |         // Default templates location (built-in fallback)
 | ||||||
|  |         'templates' => __DIR__ . '/../templates', | ||||||
|  | 
 | ||||||
|  |         // Public assets directory (built-in fallback)
 | ||||||
|  |         'public' => __DIR__ . '/../../public_html' | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     // Theme configuration defaults
 | ||||||
|  |     'default_config' => [ | ||||||
|  |         'name' => 'Unnamed Theme', | ||||||
|  |         'description' => 'A Jilo Web theme', | ||||||
|  |         'version' => '1.0.0', | ||||||
|  |         'author' => 'Lindeas Inc.', | ||||||
|  |         'screenshot' => 'screenshot.png', | ||||||
|  |         'options' => [] | ||||||
|  |     ] | ||||||
|  | ]; | ||||||
|  | @ -0,0 +1,41 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Core; | ||||||
|  | 
 | ||||||
|  | class ConfigLoader | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * @var string|null | ||||||
|  |      */ | ||||||
|  |     private static $configPath = null; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Load configuration array from a set of possible file locations. | ||||||
|  |      * | ||||||
|  |      * @param string[] $locations | ||||||
|  |      * @return array | ||||||
|  |      */ | ||||||
|  |     public static function loadConfig(array $locations): array | ||||||
|  |     { | ||||||
|  |         $configFile = null; | ||||||
|  |         foreach ($locations as $location) { | ||||||
|  |             if (file_exists($location)) { | ||||||
|  |                 $configFile = $location; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (!$configFile) { | ||||||
|  |             die('Config file not found'); | ||||||
|  |         } | ||||||
|  |         self::$configPath = $configFile; | ||||||
|  |         return require $configFile; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @return string|null | ||||||
|  |      */ | ||||||
|  |     public static function getConfigPath(): ?string | ||||||
|  |     { | ||||||
|  |         return self::$configPath; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,37 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Core; | ||||||
|  | 
 | ||||||
|  | use Exception; | ||||||
|  | use Feedback; | ||||||
|  | 
 | ||||||
|  | class DatabaseConnector | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Connect to the database using given configuration and handle errors. | ||||||
|  |      * | ||||||
|  |      * @param array $config | ||||||
|  |      * @return mixed Database connection | ||||||
|  |      */ | ||||||
|  |     public static function connect(array $config) | ||||||
|  |     { | ||||||
|  |         // Load DB classes
 | ||||||
|  |         require_once __DIR__ . '/../classes/database.php'; | ||||||
|  |         require_once __DIR__ . '/../includes/database.php'; | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             $db = connectDB($config); | ||||||
|  |             if (!$db) { | ||||||
|  |                 throw new Exception('Could not connect to database'); | ||||||
|  |             } | ||||||
|  |             return $db; | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             // Show error and exit
 | ||||||
|  |             Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to the database.', $e->getMessage())); | ||||||
|  |             include __DIR__ . '/../templates/page-header.php'; | ||||||
|  |             include __DIR__ . '/../helpers/feedback.php'; | ||||||
|  |             include __DIR__ . '/../templates/page-footer.php'; | ||||||
|  |             exit(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,53 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Core; | ||||||
|  | 
 | ||||||
|  | class HookDispatcher | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Stores all registered hooks and their callbacks. | ||||||
|  |      * @var array<string, array<callable>> | ||||||
|  |      */ | ||||||
|  |     private static array $hooks = []; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Register a callback for a given hook. | ||||||
|  |      */ | ||||||
|  |     public static function register(string $hook, callable $callback): void | ||||||
|  |     { | ||||||
|  |         if (!isset(self::$hooks[$hook])) { | ||||||
|  |             self::$hooks[$hook] = []; | ||||||
|  |         } | ||||||
|  |         self::$hooks[$hook][] = $callback; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Dispatch all callbacks for the specified hook. | ||||||
|  |      */ | ||||||
|  |     public static function dispatch(string $hook, array $context = []): void | ||||||
|  |     { | ||||||
|  |         if (!empty(self::$hooks[$hook])) { | ||||||
|  |             foreach (self::$hooks[$hook] as $callback) { | ||||||
|  |                 call_user_func($callback, $context); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Apply filters for a hook key, passing a value through all callbacks. | ||||||
|  |      * Each callback should accept the value and return a modified value. | ||||||
|  |      * | ||||||
|  |      * @param string $hook | ||||||
|  |      * @param mixed $value | ||||||
|  |      * @return mixed | ||||||
|  |      */ | ||||||
|  |     public static function applyFilters(string $hook, $value) | ||||||
|  |     { | ||||||
|  |         if (!empty(self::$hooks[$hook])) { | ||||||
|  |             foreach (self::$hooks[$hook] as $callback) { | ||||||
|  |                 $value = call_user_func($callback, $value); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return $value; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,89 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Core; | ||||||
|  | 
 | ||||||
|  | class Maintenance | ||||||
|  | { | ||||||
|  |     // Keep it simple: store the flag within the app directory
 | ||||||
|  |     public const FLAG_PATH = __DIR__ . '/../../app/.maintenance.flag'; | ||||||
|  | 
 | ||||||
|  |     public static function isEnabled(): bool | ||||||
|  |     { | ||||||
|  |         if (getenv('JILO_MAINTENANCE') === '1') { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         // Prefer DB settings if available in the current request
 | ||||||
|  |         if (isset($GLOBALS['db'])) { | ||||||
|  |             try { | ||||||
|  |                 require_once __DIR__ . '/Settings.php'; | ||||||
|  |                 $settings = new Settings($GLOBALS['db']); | ||||||
|  |                 return $settings->get('maintenance_enabled', '0') === '1'; | ||||||
|  |             } catch (\Throwable $e) { | ||||||
|  |                 // fall back to file flag
 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return file_exists(self::FLAG_PATH); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function enable(string $message = ''): bool | ||||||
|  |     { | ||||||
|  |         if (isset($GLOBALS['db'])) { | ||||||
|  |             try { | ||||||
|  |                 require_once __DIR__ . '/Settings.php'; | ||||||
|  |                 $settings = new Settings($GLOBALS['db']); | ||||||
|  |                 $ok1 = $settings->set('maintenance_enabled', '1'); | ||||||
|  |                 $ok2 = $settings->set('maintenance_message', $message); | ||||||
|  |                 return $ok1 && $ok2; | ||||||
|  |             } catch (\Throwable $e) { | ||||||
|  |                 // fall back to file flag
 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         $dir = dirname(self::FLAG_PATH); | ||||||
|  |         if (!is_dir($dir)) { | ||||||
|  |             mkdir($dir, 0755, true); | ||||||
|  |         } | ||||||
|  |         $content = $message !== '' ? $message : 'Site is under maintenance'; | ||||||
|  |         return file_put_contents(self::FLAG_PATH, $content) !== false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function disable(): bool | ||||||
|  |     { | ||||||
|  |         if (isset($GLOBALS['db'])) { | ||||||
|  |             try { | ||||||
|  |                 require_once __DIR__ . '/Settings.php'; | ||||||
|  |                 $settings = new Settings($GLOBALS['db']); | ||||||
|  |                 $ok1 = $settings->set('maintenance_enabled', '0'); | ||||||
|  |                 // keep last message for reference, optional to clear
 | ||||||
|  |                 return $ok1; | ||||||
|  |             } catch (\Throwable $e) { | ||||||
|  |                 // fall back to file flag
 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (file_exists(self::FLAG_PATH)) { | ||||||
|  |             return unlink(self::FLAG_PATH); | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function getMessage(): string | ||||||
|  |     { | ||||||
|  |         if (!self::isEnabled()) { | ||||||
|  |             return ''; | ||||||
|  |         } | ||||||
|  |         $envMsg = getenv('JILO_MAINTENANCE_MESSAGE'); | ||||||
|  |         if ($envMsg) { | ||||||
|  |             return trim($envMsg); | ||||||
|  |         } | ||||||
|  |         if (isset($GLOBALS['db'])) { | ||||||
|  |             try { | ||||||
|  |                 require_once __DIR__ . '/Settings.php'; | ||||||
|  |                 $settings = new Settings($GLOBALS['db']); | ||||||
|  |                 return (string)$settings->get('maintenance_message', ''); | ||||||
|  |             } catch (\Throwable $e) { | ||||||
|  |                 // ignore and fall back to file flag
 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         $msg = @file_get_contents(self::FLAG_PATH); | ||||||
|  |         return is_string($msg) ? trim($msg) : ''; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,30 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Core; | ||||||
|  | 
 | ||||||
|  | class MiddlewarePipeline { | ||||||
|  |     /** @var callable[] */ | ||||||
|  |     private $middlewares = []; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add a middleware to the pipeline. | ||||||
|  |      * @param callable $middleware Should return false to halt execution. | ||||||
|  |      */ | ||||||
|  |     public function add(callable $middleware): void { | ||||||
|  |         $this->middlewares[] = $middleware; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Execute all middlewares in sequence. | ||||||
|  |      * @return bool False if any middleware returns false, true otherwise. | ||||||
|  |      */ | ||||||
|  |     public function run(): bool { | ||||||
|  |         foreach ($this->middlewares as $middleware) { | ||||||
|  |             $result = call_user_func($middleware); | ||||||
|  |             if ($result === false) { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,119 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Core; | ||||||
|  | 
 | ||||||
|  | use PDO; | ||||||
|  | use Exception; | ||||||
|  | 
 | ||||||
|  | class MigrationRunner | ||||||
|  | { | ||||||
|  |     private PDO $pdo; | ||||||
|  |     private string $migrationsDir; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @param mixed $db Either a PDO instance or the application's Database wrapper | ||||||
|  |      * @param string $migrationsDir Directory containing .sql migrations | ||||||
|  |      */ | ||||||
|  |     public function __construct($db, string $migrationsDir) | ||||||
|  |     { | ||||||
|  |         // Normalize to PDO
 | ||||||
|  |         if ($db instanceof PDO) { | ||||||
|  |             $this->pdo = $db; | ||||||
|  |         } elseif (is_object($db) && method_exists($db, 'getConnection')) { | ||||||
|  |             $pdo = $db->getConnection(); | ||||||
|  |             if (!$pdo instanceof PDO) { | ||||||
|  |                 throw new Exception('Database wrapper did not return a PDO instance'); | ||||||
|  |             } | ||||||
|  |             $this->pdo = $pdo; | ||||||
|  |         } else { | ||||||
|  |             $type = is_object($db) ? get_class($db) : gettype($db); | ||||||
|  |             throw new Exception("Unsupported database type: {$type}"); | ||||||
|  |         } | ||||||
|  |         $this->migrationsDir = rtrim($migrationsDir, '/'); | ||||||
|  |         if (!is_dir($this->migrationsDir)) { | ||||||
|  |             throw new Exception("Migrations directory not found: {$this->migrationsDir}"); | ||||||
|  |         } | ||||||
|  |         $this->ensureMigrationsTable(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function ensureMigrationsTable(): void | ||||||
|  |     { | ||||||
|  |         $driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME); | ||||||
|  |         if ($driver === 'sqlite') { | ||||||
|  |             $sql = "CREATE TABLE IF NOT EXISTS migrations (
 | ||||||
|  |                 id INTEGER PRIMARY KEY AUTOINCREMENT, | ||||||
|  |                 migration TEXT NOT NULL UNIQUE, | ||||||
|  |                 applied_at TEXT NOT NULL | ||||||
|  |             )";
 | ||||||
|  |         } else { | ||||||
|  |             $sql = "CREATE TABLE IF NOT EXISTS migrations (
 | ||||||
|  |                 id INT AUTO_INCREMENT PRIMARY KEY, | ||||||
|  |                 migration VARCHAR(255) NOT NULL UNIQUE, | ||||||
|  |                 applied_at DATETIME NOT NULL | ||||||
|  |             ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4";
 | ||||||
|  |         } | ||||||
|  |         $this->pdo->exec($sql); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function listAllMigrations(): array | ||||||
|  |     { | ||||||
|  |         $files = glob($this->migrationsDir . '/*.sql'); | ||||||
|  |         sort($files, SORT_NATURAL); | ||||||
|  |         return array_map('basename', $files); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function listAppliedMigrations(): array | ||||||
|  |     { | ||||||
|  |         $stmt = $this->pdo->query('SELECT migration FROM migrations ORDER BY migration ASC'); | ||||||
|  |         return $stmt->fetchAll(PDO::FETCH_COLUMN) ?: []; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function listPendingMigrations(): array | ||||||
|  |     { | ||||||
|  |         $all = $this->listAllMigrations(); | ||||||
|  |         $applied = $this->listAppliedMigrations(); | ||||||
|  |         return array_values(array_diff($all, $applied)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function hasPendingMigrations(): bool | ||||||
|  |     { | ||||||
|  |         return count($this->listPendingMigrations()) > 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function applyPendingMigrations(): array | ||||||
|  |     { | ||||||
|  |         $pending = $this->listPendingMigrations(); | ||||||
|  |         $appliedNow = []; | ||||||
|  |         if (empty($pending)) { | ||||||
|  |             return $appliedNow; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             $this->pdo->beginTransaction(); | ||||||
|  |             foreach ($pending as $migration) { | ||||||
|  |                 $path = $this->migrationsDir . '/' . $migration; | ||||||
|  |                 $sql = file_get_contents($path); | ||||||
|  |                 if ($sql === false) { | ||||||
|  |                     throw new Exception("Unable to read migration file: {$migration}"); | ||||||
|  |                 } | ||||||
|  |                 // Split on ; at line ends, but allow inside procedures? Keep simple for our use-cases
 | ||||||
|  |                 $statements = array_filter(array_map('trim', preg_split('/;\s*\n/', $sql))); | ||||||
|  |                 foreach ($statements as $stmtSql) { | ||||||
|  |                     if ($stmtSql === '') continue; | ||||||
|  |                     $this->pdo->exec($stmtSql); | ||||||
|  |                 } | ||||||
|  |                 $ins = $this->pdo->prepare('INSERT INTO migrations (migration, applied_at) VALUES (:m, NOW())'); | ||||||
|  |                 $ins->execute([':m' => $migration]); | ||||||
|  |                 $appliedNow[] = $migration; | ||||||
|  |             } | ||||||
|  |             $this->pdo->commit(); | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             if ($this->pdo->inTransaction()) { | ||||||
|  |                 $this->pdo->rollBack(); | ||||||
|  |             } | ||||||
|  |             throw $e; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $appliedNow; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,17 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Core; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * NullLogger is a fallback for disabling logging when there is no logging plugin enabled. | ||||||
|  |  */ | ||||||
|  | class NullLogger | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * PSR-3 compatible log stub. | ||||||
|  |      * @param string $level | ||||||
|  |      * @param string $message | ||||||
|  |      * @param array $context | ||||||
|  |      */ | ||||||
|  |     public function log(string $level, string $message, array $context = []): void {} | ||||||
|  | } | ||||||
|  | @ -0,0 +1,37 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Core; | ||||||
|  | 
 | ||||||
|  | class PluginManager | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * Loads all enabled plugins from the given directory. | ||||||
|  |      * | ||||||
|  |      * @param string $pluginsDir | ||||||
|  |      * @return array<string, array{path: string, meta: array}> | ||||||
|  |      */ | ||||||
|  |     public static function load(string $pluginsDir): array | ||||||
|  |     { | ||||||
|  |         $enabled = []; | ||||||
|  |         foreach (glob($pluginsDir . '*', GLOB_ONLYDIR) as $pluginPath) { | ||||||
|  |             $manifest = $pluginPath . '/plugin.json'; | ||||||
|  |             if (!file_exists($manifest)) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             $meta = json_decode(file_get_contents($manifest), true); | ||||||
|  |             if (empty($meta['enabled'])) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             $name = basename($pluginPath); | ||||||
|  |             $enabled[$name] = [ | ||||||
|  |                 'path' => $pluginPath, | ||||||
|  |                 'meta' => $meta, | ||||||
|  |             ]; | ||||||
|  |             $bootstrap = $pluginPath . '/bootstrap.php'; | ||||||
|  |             if (file_exists($bootstrap)) { | ||||||
|  |                 include_once $bootstrap; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return $enabled; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,57 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Core; | ||||||
|  | 
 | ||||||
|  | use Session; | ||||||
|  | use Feedback; | ||||||
|  | 
 | ||||||
|  | class Router { | ||||||
|  |     /** | ||||||
|  |      * Check session validity and handle redirection for protected pages. | ||||||
|  |      * Returns current username if session is valid, null otherwise. | ||||||
|  |      */ | ||||||
|  |     public static function checkAuth(array $config, string $app_root, array $public_pages, string $page): ?string { | ||||||
|  |         // Always allow login page to be accessed
 | ||||||
|  |         if ($page === 'login') { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Check if this is a public page
 | ||||||
|  |         $isPublicPage = in_array($page, $public_pages, true); | ||||||
|  | 
 | ||||||
|  |         // For public pages, don't validate session
 | ||||||
|  |         if ($isPublicPage) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // For protected pages, check if we have a valid session
 | ||||||
|  |         $validSession = Session::isValidSession(true); | ||||||
|  | 
 | ||||||
|  |         // If session is valid, return the username
 | ||||||
|  |         if ($validSession) { | ||||||
|  |             return Session::getUsername(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // If we get here, we need to redirect to login
 | ||||||
|  |         // Only show timeout message if we had an active session before
 | ||||||
|  |         if (isset($_SESSION['LAST_ACTIVITY']) && !isset($_SESSION['session_timeout_shown'])) { | ||||||
|  |             Feedback::flash('LOGIN', 'SESSION_TIMEOUT'); | ||||||
|  |             $_SESSION['session_timeout_shown'] = true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Preserve flash messages
 | ||||||
|  |         $flash_messages = $_SESSION['flash_messages'] ?? []; | ||||||
|  |         Session::cleanup($config); | ||||||
|  |         $_SESSION['flash_messages'] = $flash_messages; | ||||||
|  | 
 | ||||||
|  |         // Build login URL with redirect if appropriate
 | ||||||
|  |         $loginUrl = $app_root . '?page=login'; | ||||||
|  |         $trimmed = trim($page, '/?'); | ||||||
|  |         if (!empty($trimmed) && !in_array($trimmed, INVALID_REDIRECT_PAGES, true)) { | ||||||
|  |             $loginUrl .= '&redirect=' . urlencode($_SERVER['REQUEST_URI']); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         header('Location: ' . $loginUrl); | ||||||
|  |         exit(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,54 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | namespace App\Core; | ||||||
|  | 
 | ||||||
|  | use PDO; | ||||||
|  | use Exception; | ||||||
|  | 
 | ||||||
|  | class Settings | ||||||
|  | { | ||||||
|  |     private PDO $pdo; | ||||||
|  | 
 | ||||||
|  |     public function __construct($db) | ||||||
|  |     { | ||||||
|  |         if ($db instanceof PDO) { | ||||||
|  |             $this->pdo = $db; | ||||||
|  |         } elseif (is_object($db) && method_exists($db, 'getConnection')) { | ||||||
|  |             $pdo = $db->getConnection(); | ||||||
|  |             if (!$pdo instanceof PDO) { | ||||||
|  |                 throw new Exception('Settings: database wrapper did not return PDO'); | ||||||
|  |             } | ||||||
|  |             $this->pdo = $pdo; | ||||||
|  |         } else { | ||||||
|  |             $type = is_object($db) ? get_class($db) : gettype($db); | ||||||
|  |             throw new Exception("Settings: unsupported database type: {$type}"); | ||||||
|  |         } | ||||||
|  |         $this->ensureTable(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private function ensureTable(): void | ||||||
|  |     { | ||||||
|  |         $driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME); | ||||||
|  |         if ($driver === 'sqlite') { | ||||||
|  |             $sql = "CREATE TABLE IF NOT EXISTS settings (\n                `key` TEXT PRIMARY KEY,\n                `value` TEXT,\n                `updated_at` TEXT NOT NULL\n            )"; | ||||||
|  |         } else { | ||||||
|  |             $sql = "CREATE TABLE IF NOT EXISTS settings (\n                `key` VARCHAR(191) NOT NULL PRIMARY KEY,\n                `value` TEXT NULL,\n                `updated_at` DATETIME NOT NULL\n            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"; | ||||||
|  |         } | ||||||
|  |         $this->pdo->exec($sql); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function get(string $key, $default = null) | ||||||
|  |     { | ||||||
|  |         $stmt = $this->pdo->prepare('SELECT `value` FROM settings WHERE `key` = :k'); | ||||||
|  |         $stmt->execute([':k' => $key]); | ||||||
|  |         $val = $stmt->fetchColumn(); | ||||||
|  |         if ($val === false) return $default; | ||||||
|  |         return $val; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public function set(string $key, $value): bool | ||||||
|  |     { | ||||||
|  |         $stmt = $this->pdo->prepare('REPLACE INTO settings (`key`, `value`, `updated_at`) VALUES (:k, :v, NOW())'); | ||||||
|  |         return (bool)$stmt->execute([':k' => $key, ':v' => $value]); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,36 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Feedback Helper | ||||||
|  |  * | ||||||
|  |  * Combines functionality to handle retrieving and displaying feedback messages. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | // Get any flash messages from previous request
 | ||||||
|  | $flash_messages = Feedback::getFlash(); | ||||||
|  | if (!empty($flash_messages)) { | ||||||
|  |     $system_messages = array_merge($system_messages ?? [], array_map(function($flash) { | ||||||
|  |         return [ | ||||||
|  |             'category' => $flash['category'], | ||||||
|  |             'key' => $flash['key'], | ||||||
|  |             'custom_message' => $flash['custom_message'] ?? null, | ||||||
|  |             'dismissible' => $flash['dismissible'] ?? false, | ||||||
|  |             'small' => $flash['small'] ?? false, | ||||||
|  |             'sanitize' => $flash['sanitize'] ?? true | ||||||
|  |         ]; | ||||||
|  |     }, $flash_messages)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Show feedback messages
 | ||||||
|  | if (isset($system_messages) && is_array($system_messages)) { | ||||||
|  |     foreach ($system_messages as $msg) { | ||||||
|  |         echo Feedback::render( | ||||||
|  |             $msg['category'], | ||||||
|  |             $msg['key'], | ||||||
|  |             $msg['custom_message'] ?? null, | ||||||
|  |             $msg['dismissible'] ?? false, | ||||||
|  |             $msg['small'] ?? false, | ||||||
|  |             $msg['sanitize'] ?? true | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,24 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Returns the user's IP address. | ||||||
|  |  * Uses global $user_IP set by Logger plugin if available, else falls back to server variables. | ||||||
|  |  * | ||||||
|  |  * @return string | ||||||
|  |  */ | ||||||
|  | function getUserIP() { | ||||||
|  |     global $user_IP; | ||||||
|  |     if (!empty($user_IP)) { | ||||||
|  |         return $user_IP; | ||||||
|  |     } | ||||||
|  |     // Fallback to HTTP headers or REMOTE_ADDR
 | ||||||
|  |     if (!empty($_SERVER['HTTP_CLIENT_IP'])) { | ||||||
|  |         return $_SERVER['HTTP_CLIENT_IP']; | ||||||
|  |     } | ||||||
|  |     if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { | ||||||
|  |         // May contain multiple IPs
 | ||||||
|  |         $parts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); | ||||||
|  |         return trim($parts[0]); | ||||||
|  |     } | ||||||
|  |     return $_SERVER['REMOTE_ADDR'] ?? ''; | ||||||
|  | } | ||||||
|  | @ -0,0 +1,15 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Returns a logger instance: plugin Log if available, otherwise NullLogger. | ||||||
|  |  * | ||||||
|  |  * @param mixed $database Database or DatabaseConnector instance. | ||||||
|  |  * @return mixed Logger instance with PSR-3 log() compatible method. | ||||||
|  |  */ | ||||||
|  | function getLoggerInstance($database) { | ||||||
|  |     if (class_exists('Log')) { | ||||||
|  |         return new Log($database); | ||||||
|  |     } | ||||||
|  |     require_once __DIR__ . '/../core/NullLogger.php'; | ||||||
|  |     return new \App\Core\NullLogger(); | ||||||
|  | } | ||||||
|  | @ -1,22 +0,0 @@ | ||||||
| <?php |  | ||||||
| 
 |  | ||||||
| function getUserIP() { |  | ||||||
|     // get directly the user IP
 |  | ||||||
|     if (!empty($_SERVER['HTTP_CLIENT_IP'])) { |  | ||||||
|         $ip = $_SERVER['HTTP_CLIENT_IP']; |  | ||||||
|     // if user is behind some proxy
 |  | ||||||
|     } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { |  | ||||||
|         $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; |  | ||||||
|     } else { |  | ||||||
|         $ip = $_SERVER['REMOTE_ADDR']; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // get only the first IP if there are more
 |  | ||||||
|     if (strpos($ip, ',') !== false) { |  | ||||||
|         $ip = explode(',', $ip)[0]; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return trim($ip); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  | @ -44,5 +44,3 @@ function switchPlatform($platform_id) { | ||||||
|     // return the new URL with the new platform_id
 |     // return the new URL with the new platform_id
 | ||||||
|     return $new_url; |     return $new_url; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,119 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Security Helper | ||||||
|  |  * | ||||||
|  |  * Security helper, to be used with all the forms in the app. | ||||||
|  |  * Implements singleton pattern for consistent state management. | ||||||
|  |  */ | ||||||
|  | class SecurityHelper { | ||||||
|  |     private static $instance = null; | ||||||
|  |     private $session; | ||||||
|  | 
 | ||||||
|  |     private function __construct() { | ||||||
|  |         // Don't start a new session, just reference the existing one
 | ||||||
|  |         $this->session = &$_SESSION; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static function getInstance() { | ||||||
|  |         if (self::$instance === null) { | ||||||
|  |             self::$instance = new SecurityHelper(); | ||||||
|  |         } | ||||||
|  |         return self::$instance; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Generate CSRF token
 | ||||||
|  |     public function generateCsrfToken() { | ||||||
|  |         if (empty($this->session['csrf_token'])) { | ||||||
|  |             $this->session['csrf_token'] = bin2hex(random_bytes(32)); | ||||||
|  |         } | ||||||
|  |         return $this->session['csrf_token']; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Verify CSRF token
 | ||||||
|  |     public function verifyCsrfToken($token) { | ||||||
|  |         if (empty($this->session['csrf_token']) || empty($token)) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         return hash_equals($this->session['csrf_token'], $token); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Sanitize string input
 | ||||||
|  |     public function sanitizeString($input) { | ||||||
|  |         if (is_string($input)) { | ||||||
|  |             return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8'); | ||||||
|  |         } | ||||||
|  |         return ''; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Validate email
 | ||||||
|  |     public function validateEmail($email) { | ||||||
|  |         return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Validate integer
 | ||||||
|  |     public function validateInt($input) { | ||||||
|  |         return filter_var($input, FILTER_VALIDATE_INT) !== false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Validate URL
 | ||||||
|  |     public function validateUrl($url) { | ||||||
|  |         return filter_var($url, FILTER_VALIDATE_URL) !== false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Sanitize array of inputs
 | ||||||
|  |     public function sanitizeArray($array, $allowedKeys = []) { | ||||||
|  |         $sanitized = []; | ||||||
|  |         foreach ($array as $key => $value) { | ||||||
|  |             if (empty($allowedKeys) || in_array($key, $allowedKeys)) { | ||||||
|  |                 if (is_array($value)) { | ||||||
|  |                     $sanitized[$key] = $this->sanitizeArray($value); | ||||||
|  |                 } else { | ||||||
|  |                     $sanitized[$key] = $this->sanitizeString($value); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return $sanitized; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Validate form data based on rules
 | ||||||
|  |     public function validateFormData($data, $rules) { | ||||||
|  |         $errors = []; | ||||||
|  |         foreach ($rules as $field => $rule) { | ||||||
|  |             if (!isset($data[$field]) && $rule['required']) { | ||||||
|  |                 $errors[$field] = "Field is required"; | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (isset($data[$field])) { | ||||||
|  |                 $value = $data[$field]; | ||||||
|  |                 switch ($rule['type']) { | ||||||
|  |                     case 'email': | ||||||
|  |                         if (!$this->validateEmail($value)) { | ||||||
|  |                             $errors[$field] = "Invalid email format"; | ||||||
|  |                         } | ||||||
|  |                         break; | ||||||
|  |                     case 'integer': | ||||||
|  |                         if (!$this->validateInt($value)) { | ||||||
|  |                             $errors[$field] = "Must be a valid integer"; | ||||||
|  |                         } | ||||||
|  |                         break; | ||||||
|  |                     case 'url': | ||||||
|  |                         if (!$this->validateUrl($value)) { | ||||||
|  |                             $errors[$field] = "Invalid URL format"; | ||||||
|  |                         } | ||||||
|  |                         break; | ||||||
|  |                     case 'string': | ||||||
|  |                         if (isset($rule['min']) && strlen($value) < $rule['min']) { | ||||||
|  |                             $errors[$field] = "Minimum length is {$rule['min']} characters"; | ||||||
|  |                         } | ||||||
|  |                         if (isset($rule['max']) && strlen($value) > $rule['max']) { | ||||||
|  |                             $errors[$field] = "Maximum length is {$rule['max']} characters"; | ||||||
|  |                         } | ||||||
|  |                         break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return $errors; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,109 @@ | ||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * Theme Asset handler | ||||||
|  |  * | ||||||
|  |  * Serves theme assets (images, CSS, JS, etc.) securely by checking if the requested | ||||||
|  |  * theme and asset path are valid and accessible. | ||||||
|  |  * | ||||||
|  |  * This is a standalone handler that doesn't require the full application initialization. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | // Set error reporting
 | ||||||
|  | error_reporting(E_ALL); | ||||||
|  | ini_set('display_errors', '1'); | ||||||
|  | 
 | ||||||
|  | // Define base path if not defined
 | ||||||
|  | if (!defined('APP_ROOT')) { | ||||||
|  |     define('APP_ROOT', dirname(__DIR__)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Basic security checks
 | ||||||
|  | if (!isset($_GET['theme']) || !preg_match('/^[a-zA-Z0-9_-]+$/', $_GET['theme'])) { | ||||||
|  |     http_response_code(400); | ||||||
|  |     exit('Invalid theme specified'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | if (!isset($_GET['path']) || empty($_GET['path'])) { | ||||||
|  |     http_response_code(400); | ||||||
|  |     exit('No asset path specified'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | $themeId = $_GET['theme']; | ||||||
|  | $assetPath = $_GET['path']; | ||||||
|  | 
 | ||||||
|  | // Validate asset path (only alphanumeric, hyphen, underscore, dot, and forward slash)
 | ||||||
|  | if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $assetPath)) { | ||||||
|  |     http_response_code(400); | ||||||
|  |     exit('Invalid asset path'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Prevent directory traversal
 | ||||||
|  | if (strpos($assetPath, '..') !== false) { | ||||||
|  |     http_response_code(400); | ||||||
|  |     exit('Invalid asset path'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Build full path to the asset
 | ||||||
|  | $themesDir = dirname(dirname(__DIR__)) . '/themes'; | ||||||
|  | $fullPath = realpath("$themesDir/$themeId/$assetPath"); | ||||||
|  | 
 | ||||||
|  | // Additional security check to ensure the path is within the themes directory
 | ||||||
|  | if ($fullPath === false) { | ||||||
|  |     http_response_code(404); | ||||||
|  |     header('Content-Type: text/plain'); | ||||||
|  |     error_log("Asset not found: $themesDir/$themeId/$assetPath"); | ||||||
|  |     exit("Asset not found: $themesDir/$themeId/$assetPath"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | if (strpos($fullPath, realpath($themesDir)) !== 0) { | ||||||
|  |     http_response_code(400); | ||||||
|  |     header('Content-Type: text/plain'); | ||||||
|  |     error_log("Security violation: Attempted to access path outside themes directory: $fullPath"); | ||||||
|  |     exit('Invalid asset path'); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Check if the file exists and is readable
 | ||||||
|  | if (!file_exists($fullPath) || !is_readable($fullPath)) { | ||||||
|  |     http_response_code(404); | ||||||
|  |     header('Content-Type: text/plain'); | ||||||
|  |     error_log("File not found or not readable: $fullPath"); | ||||||
|  |     exit("File not found or not readable: " . basename($fullPath)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Clear any previous output
 | ||||||
|  | if (ob_get_level()) { | ||||||
|  |     ob_clean(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Determine content type based on file extension
 | ||||||
|  | $extension = strtolower(pathinfo($assetPath, PATHINFO_EXTENSION)); | ||||||
|  | $contentTypes = [ | ||||||
|  |     'css'  => 'text/css', | ||||||
|  |     'js'   => 'application/javascript', | ||||||
|  |     'json' => 'application/json', | ||||||
|  |     'png'  => 'image/png', | ||||||
|  |     'jpg'  => 'image/jpeg', | ||||||
|  |     'jpeg' => 'image/jpeg', | ||||||
|  |     'gif'  => 'image/gif', | ||||||
|  |     'svg'  => 'image/svg+xml', | ||||||
|  |     'webp' => 'image/webp', | ||||||
|  |     'woff' => 'font/woff', | ||||||
|  |     'woff2' => 'font/woff2', | ||||||
|  |     'ttf'  => 'font/ttf', | ||||||
|  |     'eot'  => 'application/vnd.ms-fontobject', | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | $contentType = $contentTypes[$extension] ?? 'application/octet-stream'; | ||||||
|  | 
 | ||||||
|  | // Set proper headers
 | ||||||
|  | header('Content-Type: ' . $contentType); | ||||||
|  | header('Content-Length: ' . filesize($fullPath)); | ||||||
|  | 
 | ||||||
|  | // Cache for 24 hours (86400 seconds)
 | ||||||
|  | $expires = 86400; | ||||||
|  | header('Cache-Control: public, max-age=' . $expires); | ||||||
|  | header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $expires) . ' GMT'); | ||||||
|  | header('Pragma: cache'); | ||||||
|  | 
 | ||||||
|  | // Output the file
 | ||||||
|  | readfile($fullPath); | ||||||
|  | @ -0,0 +1,389 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Theme Helper | ||||||
|  |  * | ||||||
|  |  * Handles theme management and template/asset loading for the application. | ||||||
|  |  * Supports multiple themes with fallback to default theme when needed. | ||||||
|  |  * The default theme uses app/templates and public_html/static as fallbacks/ | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | namespace App\Helpers; | ||||||
|  | 
 | ||||||
|  | use Exception; | ||||||
|  | 
 | ||||||
|  | // Include Session class
 | ||||||
|  | require_once __DIR__ . '/../classes/session.php'; | ||||||
|  | use Session; | ||||||
|  | 
 | ||||||
|  | class Theme | ||||||
|  | { | ||||||
|  |     /** | ||||||
|  |      * @var array Theme configuration | ||||||
|  |      */ | ||||||
|  |     private static $config; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the theme configuration | ||||||
|  |      * | ||||||
|  |      * @return array | ||||||
|  |      */ | ||||||
|  |     public static function getConfig() | ||||||
|  |     { | ||||||
|  |         $configFile = __DIR__ . '/../config/theme.php'; | ||||||
|  | 
 | ||||||
|  |         // Create default config if it doesn't exist
 | ||||||
|  |         if (!file_exists($configFile)) { | ||||||
|  |             $configDir = dirname($configFile); | ||||||
|  |             if (!is_dir($configDir)) { | ||||||
|  |                 mkdir($configDir, 0755, true); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Generate the config file with proper formatting
 | ||||||
|  |             $configContent = <<<'EOT' | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Theme Configuration | ||||||
|  |  * | ||||||
|  |  * This file is auto-generated. Do not edit it manually. | ||||||
|  |  * Use the theme management interface to modify theme settings. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | return [ | ||||||
|  |     // Active theme (can be overridden by user preference)
 | ||||||
|  |     'active_theme' => 'modern', | ||||||
|  | 
 | ||||||
|  |     // Available themes with their display names
 | ||||||
|  |     'available_themes' => [ | ||||||
|  |         'default' => 'Default built-in theme', | ||||||
|  |         'modern' => 'Modern theme', | ||||||
|  |         'retro' => 'Alternative retro theme' | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     // Path configurations
 | ||||||
|  |     'paths' => [ | ||||||
|  |         // Base directory for all external themes
 | ||||||
|  |         'themes' => __DIR__ . '/../../themes', | ||||||
|  | 
 | ||||||
|  |         // Default templates location (built-in fallback)
 | ||||||
|  |         'templates' => __DIR__ . '/../templates', | ||||||
|  | 
 | ||||||
|  |         // Public assets directory (built-in fallback)
 | ||||||
|  |         'public' => __DIR__ . '/../../public_html' | ||||||
|  |     ], | ||||||
|  | 
 | ||||||
|  |     // Theme configuration defaults
 | ||||||
|  |     'default_config' => [ | ||||||
|  |         'name' => 'Unnamed Theme', | ||||||
|  |         'description' => 'A Jilo Web theme', | ||||||
|  |         'version' => '1.0.0', | ||||||
|  |         'author' => 'Lindeas Inc.', | ||||||
|  |         'screenshot' => 'screenshot.png', | ||||||
|  |         'options' => [] | ||||||
|  |     ] | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | EOT; | ||||||
|  | 
 | ||||||
|  |             file_put_contents($configFile, $configContent); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Load the configuration
 | ||||||
|  |         self::$config = require $configFile; | ||||||
|  |         return self::$config; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * @var string Current theme name | ||||||
|  |      */ | ||||||
|  |     private static $currentTheme; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize the theme system | ||||||
|  |      */ | ||||||
|  |     public static function init() | ||||||
|  |     { | ||||||
|  |         // Only load config if not already loaded
 | ||||||
|  |         if (self::$config === null) { | ||||||
|  |             try { | ||||||
|  |                 self::getConfig(); // This will create default config if needed
 | ||||||
|  |             } catch (Exception $e) { | ||||||
|  |                 error_log('Failed to load theme configuration: ' . $e->getMessage()); | ||||||
|  |                 // Fallback to default configuration
 | ||||||
|  |                 self::$config = [ | ||||||
|  |                     'active_theme' => 'modern', | ||||||
|  |                     'available_themes' => [ | ||||||
|  |                         'modern' => ['name' => 'Modern'], | ||||||
|  |                         'retro' => ['name' => 'Retro'] | ||||||
|  |                     ] | ||||||
|  |                 ]; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         self::$currentTheme = self::getCurrentThemeName(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the current theme name | ||||||
|  |      * | ||||||
|  |      * @return string | ||||||
|  |      */ | ||||||
|  |     public static function getCurrentThemeName() | ||||||
|  |     { | ||||||
|  |         // Ensure session is started
 | ||||||
|  |         if (session_status() === PHP_SESSION_NONE) { | ||||||
|  |             Session::startSession(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Check if already determined
 | ||||||
|  |         if (self::$currentTheme !== null) { | ||||||
|  |             return self::$currentTheme; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Try to get from session first
 | ||||||
|  |         $sessionTheme = isset($_SESSION['theme']) ? $_SESSION['theme'] : null; | ||||||
|  |         if ($sessionTheme && isset(self::$config['available_themes'][$sessionTheme])) { | ||||||
|  |             self::$currentTheme = $sessionTheme; | ||||||
|  |         } else { | ||||||
|  |             // Attempt to load per-user theme from DB if user is logged in and userObject is available
 | ||||||
|  |             if (Session::isValidSession() && isset($_SESSION['user_id']) && isset($GLOBALS['userObject']) && is_object($GLOBALS['userObject']) && method_exists($GLOBALS['userObject'], 'getUserTheme')) { | ||||||
|  |                 try { | ||||||
|  |                     $dbTheme = $GLOBALS['userObject']->getUserTheme((int)$_SESSION['user_id']); | ||||||
|  |                     if ($dbTheme && isset(self::$config['available_themes'][$dbTheme]) && self::themeExists($dbTheme)) { | ||||||
|  |                         // Set session and current theme to the user's stored preference
 | ||||||
|  |                         $_SESSION['theme'] = $dbTheme; | ||||||
|  |                         self::$currentTheme = $dbTheme; | ||||||
|  |                     } | ||||||
|  |                 } catch (\Throwable $e) { | ||||||
|  |                     // Ignore and continue to default fallback
 | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Fall back to default theme if still not determined
 | ||||||
|  |             if (self::$currentTheme === null) { | ||||||
|  |                 self::$currentTheme = self::$config['active_theme']; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return self::$currentTheme; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the URL for a theme asset | ||||||
|  |      * | ||||||
|  |      * @param string $themeId Theme ID | ||||||
|  |      * @param string $assetPath Path to the asset relative to theme directory (e.g., 'css/style.css') | ||||||
|  |      * @return string|null URL to the asset or null if not found | ||||||
|  |      */ | ||||||
|  |     public static function getAssetUrl($themeId, $assetPath = '') | ||||||
|  |     { | ||||||
|  |         // Clean and validate the asset path
 | ||||||
|  |         $assetPath = ltrim($assetPath, '/'); | ||||||
|  |         if (empty($assetPath)) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Only allow alphanumeric, hyphen, underscore, dot, and forward slash
 | ||||||
|  |         if (!preg_match('/^[a-zA-Z0-9_\-\.\/]+$/', $assetPath)) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Prevent directory traversal
 | ||||||
|  |         if (strpos($assetPath, '..') !== false) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $fullPath = __DIR__ . "/../../themes/$themeId/$assetPath"; | ||||||
|  |         if (!file_exists($fullPath) || !is_readable($fullPath)) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Generate URL that goes through index.php
 | ||||||
|  |         global $app_root; | ||||||
|  |         // Remove any trailing slash from app_root to avoid double slashes
 | ||||||
|  |         $baseUrl = rtrim($app_root, '/'); | ||||||
|  |         return "$baseUrl/?page=theme-asset&theme=" . urlencode($themeId) . "&path=" . urlencode($assetPath); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Set the current theme for the session | ||||||
|  |      * | ||||||
|  |      * @param string $themeName | ||||||
|  |      * @return bool | ||||||
|  |      */ | ||||||
|  |     public static function setCurrentTheme(string $themeName, bool $persist = true): bool | ||||||
|  |     { | ||||||
|  |         if (!self::themeExists($themeName)) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Update session
 | ||||||
|  |         if (Session::isValidSession()) { | ||||||
|  |             $_SESSION['theme'] = $themeName; | ||||||
|  |         } else { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Clear the current theme cache
 | ||||||
|  |         self::$currentTheme = null; | ||||||
|  | 
 | ||||||
|  |         // Persist per-user preference in DB when available and requested
 | ||||||
|  |         if ($persist && Session::isValidSession() && isset($_SESSION['user_id'])) { | ||||||
|  |             // Try to use existing user object if available
 | ||||||
|  |             if (isset($GLOBALS['userObject']) && is_object($GLOBALS['userObject']) && method_exists($GLOBALS['userObject'], 'setUserTheme')) { | ||||||
|  |                 try { | ||||||
|  |                     $GLOBALS['userObject']->setUserTheme((int)$_SESSION['user_id'], $themeName); | ||||||
|  |                 } catch (\Throwable $e) { | ||||||
|  |                     // Non-fatal: keep session theme even if DB save fails
 | ||||||
|  |                     error_log('Failed to persist user theme: ' . $e->getMessage()); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         self::$currentTheme = $themeName; | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if a theme exists | ||||||
|  |      * | ||||||
|  |      * @param string $themeName | ||||||
|  |      * @return bool | ||||||
|  |      */ | ||||||
|  |     public static function themeExists(string $themeName): bool | ||||||
|  |     { | ||||||
|  |         // Default theme always exists as it uses core templates
 | ||||||
|  |         if ($themeName === 'default') { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $themePath = self::getThemePath($themeName); | ||||||
|  |         return is_dir($themePath) && file_exists("$themePath/config.php"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the path to a theme | ||||||
|  |      * | ||||||
|  |      * @param string|null $themeName | ||||||
|  |      * @return string | ||||||
|  |      */ | ||||||
|  |     public static function getThemePath(?string $themeName = null): string | ||||||
|  |     { | ||||||
|  |         $themeName = $themeName ?? self::getCurrentThemeName(); | ||||||
|  |         $config = self::getConfig(); | ||||||
|  |         return rtrim($config['paths']['themes'], '/') . "/$themeName"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the URL for a theme asset | ||||||
|  |      * | ||||||
|  |      * @param string $path | ||||||
|  |      * @param bool $includeVersion | ||||||
|  |      * @return string | ||||||
|  |      */ | ||||||
|  |     public static function asset($path, $includeVersion = false) | ||||||
|  |     { | ||||||
|  |         $themeName = self::getCurrentThemeName(); | ||||||
|  |         $config = self::getConfig(); | ||||||
|  |         $baseUrl = rtrim($GLOBALS['app_root'] ?? '', '/'); | ||||||
|  | 
 | ||||||
|  |         // For non-default themes, use theme assets
 | ||||||
|  |         if ($themeName !== 'default') { | ||||||
|  |             $assetPath = "/themes/{$themeName}/assets/" . ltrim($path, '/'); | ||||||
|  | 
 | ||||||
|  |             // Add version query string for cache busting
 | ||||||
|  |             if ($includeVersion) { | ||||||
|  |                 $version = self::getThemeVersion($themeName); | ||||||
|  |                 $assetPath .= (strpos($assetPath, '?') !== false ? '&' : '?') . 'v=' . $version; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             // For default theme, use public_html directly
 | ||||||
|  |             $assetPath = '/' . ltrim($path, '/'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $baseUrl . $assetPath; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Include a theme template file | ||||||
|  |      * | ||||||
|  |      * @param string $template Template name without .php extension | ||||||
|  |      * @return void | ||||||
|  |      */ | ||||||
|  |     public static function include($template) | ||||||
|  |     { | ||||||
|  |         global $config; | ||||||
|  |         $config = $config ?? []; | ||||||
|  | 
 | ||||||
|  |         $themeConfig = self::getConfig(); | ||||||
|  |         $themeName = self::getCurrentThemeName(); | ||||||
|  | 
 | ||||||
|  |         // We need this here, otherwise because this helper
 | ||||||
|  |         // between index and the views breaks the session vars
 | ||||||
|  |         extract($GLOBALS, EXTR_SKIP | EXTR_REFS); | ||||||
|  | 
 | ||||||
|  |         // Ensure config is always available in templates
 | ||||||
|  |         $config = array_merge($config, $themeConfig); | ||||||
|  | 
 | ||||||
|  |         // For non-default themes, look in the theme directory first
 | ||||||
|  |         if ($themeName !== 'default') { | ||||||
|  |             $themePath = $config['paths']['themes'] . '/' . $themeName . '/views/' . $template . '.php'; | ||||||
|  |             if (file_exists($themePath)) { | ||||||
|  |                 include $themePath; | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Fallback to default template location
 | ||||||
|  |         $defaultPath = $config['paths']['templates'] . '/' . $template . '.php'; | ||||||
|  |         if (file_exists($defaultPath)) { | ||||||
|  |             include $defaultPath; | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Log error if template not found
 | ||||||
|  |         error_log("Template not found: {$template} in theme: {$themeName}"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get all available themes | ||||||
|  |      * | ||||||
|  |      * @return array | ||||||
|  |      */ | ||||||
|  |     public static function getAvailableThemes(): array | ||||||
|  |     { | ||||||
|  |         $config = self::getConfig(); | ||||||
|  |         $availableThemes = $config['available_themes'] ?? []; | ||||||
|  |         $themes = []; | ||||||
|  | 
 | ||||||
|  |         // Add default theme if not already present
 | ||||||
|  |         if (!isset($availableThemes['default'])) { | ||||||
|  |             $availableThemes['default'] = 'Default built-in theme'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Verify each theme exists and has a config file
 | ||||||
|  |         $themesDir = $config['paths']['themes'] ?? (__DIR__ . '/../../themes'); | ||||||
|  |         foreach ($availableThemes as $id => $name) { | ||||||
|  |             if ($id === 'default' || (is_dir("$themesDir/$id") && file_exists("$themesDir/$id/config.php"))) { | ||||||
|  |                 $themes[$id] = $name; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return $themes; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Initialize the theme system
 | ||||||
|  | Theme::init(); | ||||||
|  | @ -11,5 +11,3 @@ if (!isset($until_time) || (isset($until_time) && $until_time == '')) { | ||||||
| } else { | } else { | ||||||
|     $time_range_specified = true; |     $time_range_specified = true; | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,5 @@ | ||||||
|  | <?php | ||||||
|  | // Pages that should not be used as redirect targets
 | ||||||
|  | const INVALID_REDIRECT_PAGES = [ | ||||||
|  |     '', 'login', 'logout', 'register', 'dashboard', '/' | ||||||
|  | ]; | ||||||
|  | @ -0,0 +1,52 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | require_once __DIR__ . '/../helpers/security.php'; | ||||||
|  | 
 | ||||||
|  | function applyCsrfMiddleware() { | ||||||
|  |     global $logObject, $user_IP; | ||||||
|  |     $security = SecurityHelper::getInstance(); | ||||||
|  | 
 | ||||||
|  |     // Skip CSRF check for GET requests
 | ||||||
|  |     if ($_SERVER['REQUEST_METHOD'] === 'GET') { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Skip CSRF check for initial login, registration, and 2FA verification attempts
 | ||||||
|  |     if ($_SERVER['REQUEST_METHOD'] === 'POST' && | ||||||
|  |         isset($_GET['page']) && isset($_GET['action']) && | ||||||
|  |         $_GET['page'] === 'login' && $_GET['action'] === 'verify' && | ||||||
|  |         isset($_SESSION['2fa_pending_user_id'])) { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Skip CSRF check for initial login and registration attempts
 | ||||||
|  |     if ($_SERVER['REQUEST_METHOD'] === 'POST' && | ||||||
|  |         isset($_GET['page']) && | ||||||
|  |         in_array($_GET['page'], ['login', 'register']) && | ||||||
|  |         !isset($_SESSION['username'])) { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Check CSRF token for all other POST requests
 | ||||||
|  |     if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||||||
|  |         // Check for token in POST data or headers
 | ||||||
|  |         $token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; | ||||||
|  |         if (!$security->verifyCsrfToken($token)) { | ||||||
|  |             // Log CSRF attempt
 | ||||||
|  |             $ipAddress = $user_IP; | ||||||
|  |             $logMessage = sprintf( | ||||||
|  |                 "CSRF attempt detected - IP: %s, Page: %s, User: %s", | ||||||
|  |                 $ipAddress, | ||||||
|  |                 $_GET['page'] ?? 'unknown', | ||||||
|  |                 $_SESSION['username'] ?? 'anonymous' | ||||||
|  |             ); | ||||||
|  |             $logObject->log('error', $logMessage, ['user_id' => null, 'scope' => 'system']); | ||||||
|  | 
 | ||||||
|  |             // Return error message
 | ||||||
|  |             http_response_code(403); | ||||||
|  |             die('Invalid CSRF token. Please try again.'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | @ -0,0 +1,4 @@ | ||||||
|  | <?php | ||||||
|  | $token = SecurityHelper::getInstance()->generateCsrfToken(); | ||||||
|  | ?>
 | ||||||
|  | <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token) ?>" /> | ||||||
|  | @ -1,62 +1,63 @@ | ||||||
| <?php | <?php | ||||||
| 
 | 
 | ||||||
| // connect to database
 | // connect to database
 | ||||||
| function connectDB($config, $database = '', $dbFile = '', $platformId = '') { | function connectDB($config) { | ||||||
| 
 |     // sqlite database file
 | ||||||
|     // connecting ti a jilo sqlite database
 |     if ($config['db_type'] === 'sqlite') { | ||||||
|     if ($database === 'jilo') { |  | ||||||
|         try { |         try { | ||||||
|  |             $dbFile = $config['sqlite']['sqlite_file'] ?? null; | ||||||
|             if (!$dbFile || !file_exists($dbFile)) { |             if (!$dbFile || !file_exists($dbFile)) { | ||||||
|                 throw new Exception(getError("Invalid platform ID \"{$platformId}\", database file \"{$dbFile}\" not found.")); |                 throw new Exception(getError("Database file \"{$dbFile}\"not found.")); | ||||||
|             } |             } | ||||||
|             $db = new Database([ |             $db = new Database([ | ||||||
|                 'type'		=> 'sqlite', |                 'type'		=> $config['db_type'], | ||||||
|                 'dbFile'	=> $dbFile, |                 'dbFile'	=> $dbFile, | ||||||
|             ]); |             ]); | ||||||
|             return ['db' => $db, 'error' => null]; |             $pdo = $db->getConnection(); | ||||||
|         } catch (Exception $e) { |         } catch (Exception $e) { | ||||||
|             return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())]; |             Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to DB.', $e->getMessage())); | ||||||
|  |             return false; | ||||||
|         } |         } | ||||||
|  |         return $db; | ||||||
| 
 | 
 | ||||||
|     // connecting to a jilo-web database of the web app
 |     // mysql/mariadb database
 | ||||||
|  |     } elseif ($config['db_type'] === 'mysql' || $config['db_type'] === 'mariadb') { | ||||||
|  |         $db = new Database([ | ||||||
|  |             'type'		=> $config['db_type'], | ||||||
|  |             'host'		=> $config['sql']['sql_host'] ?? 'localhost', | ||||||
|  |             'port'		=> $config['sql']['sql_port'] ?? '3306', | ||||||
|  |             'dbname'		=> $config['sql']['sql_database'], | ||||||
|  |             'user'		=> $config['sql']['sql_username'], | ||||||
|  |             'password'		=> $config['sql']['sql_password'], | ||||||
|  |         ]); | ||||||
|  |         try { | ||||||
|  |             $pdo = $db->getConnection(); | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             Feedback::flash('ERROR', 'DEFAULT', getError('Error connecting to DB.', $e->getMessage())); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         return $db; | ||||||
|  | 
 | ||||||
|  |     // unknown database
 | ||||||
|     } else { |     } else { | ||||||
| 
 |         Feedback::flash('ERROR', 'DEFAULT', getError("Error: unknown database type \"{$config['db_type']}\"")); | ||||||
|         // sqlite database file
 |         return false; | ||||||
|         if ($config['db']['db_type'] === 'sqlite') { |  | ||||||
|             try { |  | ||||||
|                 $db = new Database([ |  | ||||||
|                     'type'	=> $config['db']['db_type'], |  | ||||||
|                     'dbFile'	=> $config['db']['sqlite_file'], |  | ||||||
|                 ]); |  | ||||||
|                 $pdo = $db->getConnection(); |  | ||||||
|                 return ['db' => $db, 'error' => null]; |  | ||||||
|             } catch (Exception $e) { |  | ||||||
|                 return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())]; |  | ||||||
|             } |  | ||||||
|         // mysql/mariadb database
 |  | ||||||
|         } elseif ($config['db']['db_type'] === 'mysql' || $config['db']['db_type'] === 'mariadb') { |  | ||||||
|             try { |  | ||||||
|                 $db = new Database([ |  | ||||||
|                     'type'	=> $config['db']['db_type'], |  | ||||||
|                     'host'	=> $config['db']['sql_host'] ?? 'localhost', |  | ||||||
|                     'port'	=> $config['db']['sql_port'] ?? '3306', |  | ||||||
|                     'dbname'	=> $config['db']['sql_database'], |  | ||||||
|                     'user'	=> $config['db']['sql_username'], |  | ||||||
|                     'password'	=> $config['db']['sql_password'], |  | ||||||
|                 ]); |  | ||||||
|                 $pdo = $db->getConnection(); |  | ||||||
|                 return ['db' => $db, 'error' => null]; |  | ||||||
|             } catch (Exception $e) { |  | ||||||
|                 return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())]; |  | ||||||
|             } |  | ||||||
|         // unknown database
 |  | ||||||
|         } else { |  | ||||||
|             $error = "Error: unknow database type \"{$config['db']['db_type']}\""; |  | ||||||
|             Messages::flash('ERROR', 'DEFAULT', $error); |  | ||||||
|             exit(); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| ?>
 | 
 | ||||||
|  | // connect to Jilo database
 | ||||||
|  | function connectJiloDB($config, $dbFile = '', $platformId = '') { | ||||||
|  |     try { | ||||||
|  |         if (!$dbFile || !file_exists($dbFile)) { | ||||||
|  |             throw new Exception(getError("Invalid platform ID \"{$platformId}\", database file \"{$dbFile}\" not found.")); | ||||||
|  |         } | ||||||
|  |         $db = new Database([ | ||||||
|  |             'type'		=> 'sqlite', | ||||||
|  |             'dbFile'	=> $dbFile, | ||||||
|  |         ]); | ||||||
|  |         return ['db' => $db, 'error' => null]; | ||||||
|  |     } catch (Exception $e) { | ||||||
|  |         return ['db' => null, 'error' => getError('Error connecting to DB.', $e->getMessage())]; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -38,5 +38,3 @@ function renderMessage(&$message, $type, $unset = false) { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -1,8 +0,0 @@ | ||||||
| <?php |  | ||||||
| if (isset($messages) && is_array($messages)) { |  | ||||||
|     foreach ($messages as $msg) { |  | ||||||
|         echo Messages::render($msg['category'], $msg['key'], $msg['custom_message'] ?? null, $msg['dismissible'] ?? false, $msg['small'] ?? false); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  | @ -1,17 +0,0 @@ | ||||||
| <?php |  | ||||||
| 
 |  | ||||||
| // Get any flash messages from previous request
 |  | ||||||
| $flash_messages = Messages::getFlash(); |  | ||||||
| if (!empty($flash_messages)) { |  | ||||||
|     $messages = array_merge($messages, array_map(function($flash) { |  | ||||||
|         return [ |  | ||||||
|             'category' => $flash['category'], |  | ||||||
|             'key' => $flash['key'], |  | ||||||
|             'custom_message' => $flash['custom_message'] ?? null, |  | ||||||
|             'dismissible' => $flash['dismissible'] ?? false, |  | ||||||
|             'small' => $flash['small'] ?? false |  | ||||||
|         ]; |  | ||||||
|     }, $flash_messages)); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  | @ -0,0 +1,58 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | require_once __DIR__ . '/../classes/ratelimiter.php'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Rate limit middleware for page requests | ||||||
|  |  *  | ||||||
|  |  * @param Database $database Database connection | ||||||
|  |  * @param string $endpoint The endpoint being accessed | ||||||
|  |  * @param int|null $userId Current user ID if authenticated | ||||||
|  |  * @param RateLimiter|null $existingRateLimiter Optional existing RateLimiter instance | ||||||
|  |  * @return bool True if request is allowed, false if rate limited | ||||||
|  |  */ | ||||||
|  | function checkRateLimit($database, $endpoint, $userId = null, $existingRateLimiter = null) { | ||||||
|  |     global $app_root, $user_IP; | ||||||
|  |     $isTest = defined('PHPUNIT_RUNNING'); | ||||||
|  |     $rateLimiter = $existingRateLimiter ?? new RateLimiter($database); | ||||||
|  |     $ipAddress = $user_IP; | ||||||
|  | 
 | ||||||
|  |     // Check if request is allowed
 | ||||||
|  |     if (!$rateLimiter->isPageRequestAllowed($ipAddress, $endpoint, $userId)) { | ||||||
|  |         // Get remaining requests for error message
 | ||||||
|  |         $remaining = $rateLimiter->getRemainingPageRequests($ipAddress, $endpoint, $userId); | ||||||
|  | 
 | ||||||
|  |         if (!$isTest) { | ||||||
|  |             // Set rate limit headers
 | ||||||
|  |             header('X-RateLimit-Remaining: ' . $remaining); | ||||||
|  |             header('X-RateLimit-Reset: ' . (time() + 60)); // Reset in 1 minute
 | ||||||
|  | 
 | ||||||
|  |             // Return 429 Too Many Requests
 | ||||||
|  |             http_response_code(429); | ||||||
|  | 
 | ||||||
|  |             // If AJAX request, return JSON
 | ||||||
|  |             if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&  | ||||||
|  |                 strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') { | ||||||
|  |                 header('Content-Type: application/json'); | ||||||
|  |                 echo json_encode([ | ||||||
|  |                     'success' => false, | ||||||
|  |                     'message' => 'Too many requests. Please try again in a minute.', | ||||||
|  |                     'messageData' => Feedback::getMessageData('ERROR', 'DEFAULT', 'Too many requests. Please try again in a minute.', true) | ||||||
|  |                 ]); | ||||||
|  |             } else { | ||||||
|  |                 // For regular requests, set flash message and redirect
 | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', 'Too many requests. Please try again in a minute.', true); | ||||||
|  |                 header('Location: ' . htmlspecialchars($app_root)); | ||||||
|  |             } | ||||||
|  |             exit; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // In test mode, just set the flash message
 | ||||||
|  |         Feedback::flash('ERROR', 'DEFAULT', 'Too many requests. Please try again in a minute.', true); | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Record this request
 | ||||||
|  |     $rateLimiter->recordPageRequest($ipAddress, $endpoint); | ||||||
|  |     return true; | ||||||
|  | } | ||||||
|  | @ -2,7 +2,7 @@ | ||||||
| 
 | 
 | ||||||
| // sanitize all input vars that may end up in URLs or forms
 | // sanitize all input vars that may end up in URLs or forms
 | ||||||
| 
 | 
 | ||||||
| $platform_id = htmlspecialchars($_REQUEST['platform']); | $platform_id = htmlspecialchars($_REQUEST['platform'] ?? ''); | ||||||
| if (isset($_REQUEST['page'])) { | if (isset($_REQUEST['page'])) { | ||||||
|     $page = htmlspecialchars($_REQUEST['page']); |     $page = htmlspecialchars($_REQUEST['page']); | ||||||
| } else { | } else { | ||||||
|  | @ -59,6 +59,3 @@ if (isset($_POST['check_period'])) { | ||||||
| if (isset($_POST['name'])) { | if (isset($_POST['name'])) { | ||||||
|     $name = htmlspecialchars($_POST['name']); |     $name = htmlspecialchars($_POST['name']); | ||||||
| } | } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,117 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Security Headers Middleware | ||||||
|  |  * | ||||||
|  |  * Sets various security headers to protect against common web vulnerabilities: | ||||||
|  |  * - HSTS: Force HTTPS connections | ||||||
|  |  * - CSP: Content Security Policy to prevent XSS and other injection attacks | ||||||
|  |  * - X-Frame-Options: Prevent clickjacking | ||||||
|  |  * - X-Content-Type-Options: Prevent MIME-type sniffing | ||||||
|  |  * - Referrer-Policy: Control referrer information | ||||||
|  |  * - Permissions-Policy: Control browser features | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | function applySecurityHeaders($testMode = false) { | ||||||
|  |     $headers = []; | ||||||
|  | 
 | ||||||
|  |     // Get current page
 | ||||||
|  |     $current_page = $_GET['page'] ?? 'dashboard'; | ||||||
|  | 
 | ||||||
|  |     // Define pages that need media access
 | ||||||
|  |     $media_enabled_pages = [ | ||||||
|  |         // 'conference' => ['camera', 'microphone'],
 | ||||||
|  |         // 'call' => ['microphone'],
 | ||||||
|  |         // Add more pages and their required permissions as needed
 | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     // Strict Transport Security (HSTS)
 | ||||||
|  |     // Only enable if HTTPS is properly configured
 | ||||||
|  |     if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { | ||||||
|  |         $headers[] = 'Strict-Transport-Security: max-age=31536000; includeSubDomains; preload'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Content Security Policy (CSP)
 | ||||||
|  |     $csp = [ | ||||||
|  |         "default-src 'self'", | ||||||
|  |         "script-src 'self' 'unsafe-inline' 'unsafe-eval'", // Required for Bootstrap and jQuery
 | ||||||
|  |         "style-src 'self' 'unsafe-inline' https://use.fontawesome.com", // Allow FontAwesome CSS
 | ||||||
|  |         "img-src 'self' data:", // Allow data: URLs for images
 | ||||||
|  |         "font-src 'self' https://use.fontawesome.com", // Allow FontAwesome fonts
 | ||||||
|  |         "connect-src 'self'", | ||||||
|  |         "frame-ancestors 'none'", // Equivalent to X-Frame-Options: DENY
 | ||||||
|  |         "form-action 'self'", | ||||||
|  |         "base-uri 'self'", | ||||||
|  |         "upgrade-insecure-requests" // Force HTTPS for all requests
 | ||||||
|  |     ]; | ||||||
|  |     $headers[] = "Content-Security-Policy: " . implode('; ', $csp); | ||||||
|  | 
 | ||||||
|  |     // X-Frame-Options (legacy support)
 | ||||||
|  |     $headers[] = 'X-Frame-Options: DENY'; | ||||||
|  | 
 | ||||||
|  |     // X-Content-Type-Options
 | ||||||
|  |     $headers[] = 'X-Content-Type-Options: nosniff'; | ||||||
|  | 
 | ||||||
|  |     // X-XSS-Protection
 | ||||||
|  |     $headers[] = 'X-XSS-Protection: 1; mode=block'; | ||||||
|  | 
 | ||||||
|  |     // Referrer-Policy
 | ||||||
|  |     $headers[] = 'Referrer-Policy: strict-origin-when-cross-origin'; | ||||||
|  | 
 | ||||||
|  |     // Permissions-Policy
 | ||||||
|  |     $permissions = [ | ||||||
|  |         'geolocation=()', | ||||||
|  |         'payment=()', | ||||||
|  |         'usb=()', | ||||||
|  |         'accelerometer=()', | ||||||
|  |         'autoplay=()', | ||||||
|  |         'document-domain=()', | ||||||
|  |         'encrypted-media=()', | ||||||
|  |         'fullscreen=(self)', | ||||||
|  |         'magnetometer=()', | ||||||
|  |         'midi=()', | ||||||
|  |         'sync-xhr=(self)', | ||||||
|  |         'usb=()' | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     // Add camera/microphone permissions based on current page
 | ||||||
|  |     $camera_allowed = false; | ||||||
|  |     $microphone_allowed = false; | ||||||
|  | 
 | ||||||
|  |     if (isset($media_enabled_pages[$current_page])) { | ||||||
|  |         $allowed_media = $media_enabled_pages[$current_page]; | ||||||
|  |         if (in_array('camera', $allowed_media)) { | ||||||
|  |             $camera_allowed = true; | ||||||
|  |         } | ||||||
|  |         if (in_array('microphone', $allowed_media)) { | ||||||
|  |             $microphone_allowed = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Add media permissions
 | ||||||
|  |     $permissions[] = $camera_allowed ? 'camera=(self)' : 'camera=()'; | ||||||
|  |     $permissions[] = $microphone_allowed ? 'microphone=(self)' : 'microphone=()'; | ||||||
|  | 
 | ||||||
|  |     $headers[] = 'Permissions-Policy: ' . implode(', ', $permissions); | ||||||
|  | 
 | ||||||
|  |     // Clear PHP version
 | ||||||
|  |     if (!$testMode) { | ||||||
|  |         header_remove('X-Powered-By'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Prevent caching of sensitive pages
 | ||||||
|  |     if (in_array($current_page, ['login', 'register', 'profile', 'security'])) { | ||||||
|  |         $headers[] = 'Cache-Control: no-store, no-cache, must-revalidate, max-age=0'; | ||||||
|  |         $headers[] = 'Pragma: no-cache'; | ||||||
|  |         $headers[] = 'Expires: ' . gmdate('D, d M Y H:i:s', time() - 3600) . ' GMT'; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ($testMode) { | ||||||
|  |         return $headers; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Apply headers in production
 | ||||||
|  |     foreach ($headers as $header) { | ||||||
|  |         header($header); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -2,35 +2,49 @@ | ||||||
| 
 | 
 | ||||||
| // Message strings for translation
 | // Message strings for translation
 | ||||||
| return [ | return [ | ||||||
|  |     'ERROR' => [ | ||||||
|  |         'CSRF_INVALID' => 'Invalid security token. Please try again.', | ||||||
|  |         'INVALID_ACTION' => 'Invalid action requested.', | ||||||
|  |         'DEFAULT' => 'An error occurred. Please try again.', | ||||||
|  |     ], | ||||||
|     'LOGIN' => [ |     'LOGIN' => [ | ||||||
|         'LOGIN_SUCCESS' => 'Login successful.', |         'LOGIN_SUCCESS' => 'Login successful.', | ||||||
|         'LOGIN_FAILED' => 'Login failed. Please check your credentials.', |         'LOGIN_FAILED' => 'Login failed. Please check your credentials.', | ||||||
|         'LOGOUT_SUCCESS' => 'Logout successful. You can log in again.', |         'LOGOUT_SUCCESS' => 'Logout successful. You can log in again.', | ||||||
|  |         'SESSION_TIMEOUT' => 'Your session has expired. Please log in again.', | ||||||
|         'IP_BLACKLISTED' => 'Access denied. Your IP address is blacklisted.', |         'IP_BLACKLISTED' => 'Access denied. Your IP address is blacklisted.', | ||||||
|         'IP_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.', |         'IP_NOT_WHITELISTED' => 'Access denied. Your IP address is not whitelisted.', | ||||||
|         'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.', |         'TOO_MANY_ATTEMPTS' => 'Too many login attempts. Please try again later.', | ||||||
|     ], |     ], | ||||||
|     'SECURITY' => [ |  | ||||||
|         'WHITELIST_ADD_SUCCESS' => 'IP address successfully added to whitelist.', |  | ||||||
|         'WHITELIST_ADD_ERROR' => 'Failed to add IP to whitelist. Please check the IP format.', |  | ||||||
|         'WHITELIST_REMOVE_SUCCESS' => 'IP address successfully removed from whitelist.', |  | ||||||
|         'WHITELIST_REMOVE_ERROR' => 'Failed to remove IP from whitelist.', |  | ||||||
|         'BLACKLIST_ADD_SUCCESS' => 'IP address successfully added to blacklist.', |  | ||||||
|         'BLACKLIST_ADD_ERROR' => 'Failed to add IP to blacklist. Please check the IP format.', |  | ||||||
|         'BLACKLIST_REMOVE_SUCCESS' => 'IP address successfully removed from blacklist.', |  | ||||||
|         'BLACKLIST_REMOVE_ERROR' => 'Failed to remove IP from blacklist.', |  | ||||||
|         'RATE_LIMIT_INFO' => 'Rate limiting is active. This helps protect against brute force attacks.', |  | ||||||
|         'PERMISSION_DENIED' => 'Permission denied. You do not have the required rights.', |  | ||||||
|         'IP_REQUIRED' => 'IP address is required.', |  | ||||||
|     ], |  | ||||||
|     'REGISTER' => [ |     'REGISTER' => [ | ||||||
|         'SUCCESS' => 'Registration successful. You can log in now.', |         'SUCCESS' => 'Registration successful. You can log in now.', | ||||||
|         'FAILED' => 'Registration failed: %s', |         'FAILED' => 'Registration failed: %s', | ||||||
|         'DISABLED' => 'Registration is disabled.', |         'DISABLED' => 'Registration is disabled.', | ||||||
|     ], |     ], | ||||||
|  |     'SECURITY' => [ | ||||||
|  |         'WHITELIST_ADD_SUCCESS' => 'IP address successfully added to whitelist.', | ||||||
|  |         'WHITELIST_ADD_FAILED' => 'Failed to add IP to whitelist.', | ||||||
|  |         'WHITELIST_ADD_ERROR_IP' => 'Failed to add IP to whitelist. Please check the IP format.', | ||||||
|  |         'WHITELIST_REMOVE_SUCCESS' => 'IP address successfully removed from whitelist.', | ||||||
|  |         'WHITELIST_REMOVE_FAILED' => 'Failed to remove IP from whitelist.', | ||||||
|  |         'BLACKLIST_ADD_SUCCESS' => 'IP address successfully added to blacklist.', | ||||||
|  |         'BLACKLIST_ADD_FAILED' => 'Failed to add IP to blacklist.', | ||||||
|  |         'BLACKLIST_ADD_ERROR_IP' => 'Failed to add IP to blacklist. Please check the IP format.', | ||||||
|  |         'BLACKLIST_REMOVE_SUCCESS' => 'IP address successfully removed from blacklist.', | ||||||
|  |         'BLACKLIST_REMOVE_FAILED' => 'Failed to remove IP from blacklist.', | ||||||
|  |         'RATE_LIMIT_INFO' => 'Rate limiting is active. This helps protect against brute force attacks.', | ||||||
|  |         'PERMISSION_DENIED' => 'Permission denied. You do not have the required rights.', | ||||||
|  |         'IP_REQUIRED' => 'IP address is required.', | ||||||
|  |     ], | ||||||
|  |     'THEME' => [ | ||||||
|  |         'THEME_CHANGE_SUCCESS' => 'Theme has been changed successfully.', | ||||||
|  |         'THEME_CHANGE_FAILED' => 'Failed to change theme. The selected theme may not be available.', | ||||||
|  |     ], | ||||||
|     'SYSTEM' => [ |     'SYSTEM' => [ | ||||||
|         'DB_ERROR' => 'Error connecting to the database: %s', |         'DB_ERROR' => 'Error connecting to the database: %s', | ||||||
|         'DB_CONNECT_ERROR' => 'Error connecting to DB: %s', |         'DB_CONNECT_ERROR' => 'Error connecting to DB: %s', | ||||||
|         'DB_UNKNOWN_TYPE' => 'Error: unknown database type "%s"', |         'DB_UNKNOWN_TYPE' => 'Error: unknown database type "%s"', | ||||||
|  |         'MIGRATIONS_PENDING' => '%s', | ||||||
|  |         'MAINTENANCE_ON' => 'Maintenance mode is enabled. Regular users see a maintenance page.', | ||||||
|     ], |     ], | ||||||
| ]; | ]; | ||||||
|  | @ -0,0 +1,148 @@ | ||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * Admin tools controller | ||||||
|  |  * | ||||||
|  |  * Allows superusers to: | ||||||
|  |  * - Enable/disable maintenance mode | ||||||
|  |  * - Run database migrations | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | // Security and CSRF
 | ||||||
|  | require_once __DIR__ . '/../helpers/security.php'; | ||||||
|  | $security = SecurityHelper::getInstance(); | ||||||
|  | 
 | ||||||
|  | // Must be logged in
 | ||||||
|  | if (!Session::isValidSession()) { | ||||||
|  |     header('Location: ' . $app_root . '?page=login'); | ||||||
|  |     exit; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Must be superuser
 | ||||||
|  | $canAdmin = false; | ||||||
|  | if (isset($userId) && isset($userObject) && method_exists($userObject, 'hasRight')) { | ||||||
|  |     $canAdmin = ($userId === 1) || (bool)$userObject->hasRight($userId, 'superuser'); | ||||||
|  | } | ||||||
|  | if (!$canAdmin) { | ||||||
|  |     Feedback::flash('SECURITY', 'PERMISSION_DENIED'); | ||||||
|  |     header('Location: ' . $app_root); | ||||||
|  |     exit; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Get any old feedback messages
 | ||||||
|  | include __DIR__ . '/../helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  | // Handle actions
 | ||||||
|  | $action = $_POST['action'] ?? ''; | ||||||
|  | 
 | ||||||
|  | // AJAX: view migration file contents
 | ||||||
|  | if ($action === 'read_migration') { | ||||||
|  |     header('Content-Type: application/json'); | ||||||
|  | 
 | ||||||
|  |     // CSRF check
 | ||||||
|  |     $csrfHeader = $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; | ||||||
|  |     $csrfToken = $_POST['csrf_token'] ?? $csrfHeader; | ||||||
|  |     if (!$security->verifyCsrfToken($csrfToken)) { | ||||||
|  |         echo json_encode(['success' => false, 'error' => 'Invalid CSRF token']); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Permission check
 | ||||||
|  |     if (!$canAdmin) { | ||||||
|  |         echo json_encode(['success' => false, 'error' => 'Permission denied']); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Validate filename to avoid traversal
 | ||||||
|  |     $filename = basename($_POST['filename'] ?? ''); | ||||||
|  |     if ($filename === '' || !preg_match('/^[A-Za-z0-9_\-]+\.sql$/', $filename)) { | ||||||
|  |         echo json_encode(['success' => false, 'error' => 'Invalid filename']); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     $migrationsDir = __DIR__ . '/../../doc/database/migrations'; | ||||||
|  |     $path = realpath($migrationsDir . '/' . $filename); | ||||||
|  |     if ($path === false || strpos($path, realpath($migrationsDir)) !== 0) { | ||||||
|  |         echo json_encode(['success' => false, 'error' => 'File not found']); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     $content = @file_get_contents($path); | ||||||
|  |     if ($content === false) { | ||||||
|  |         echo json_encode(['success' => false, 'error' => 'Could not read file']); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     echo json_encode(['success' => true, 'name' => $filename, 'content' => $content]); | ||||||
|  |     exit; | ||||||
|  | } | ||||||
|  | if ($action !== '') { | ||||||
|  |     if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) { | ||||||
|  |         Feedback::flash('SECURITY', 'CSRF_INVALID'); | ||||||
|  |         header('Location: ' . $app_root . '?page=admin-tools'); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |         if ($action === 'maintenance_on') { | ||||||
|  |             require_once __DIR__ . '/../core/Maintenance.php'; | ||||||
|  |             $msg = trim($_POST['maintenance_message'] ?? ''); | ||||||
|  |             \App\Core\Maintenance::enable($msg); | ||||||
|  |             Feedback::flash('NOTICE', 'DEFAULT', 'Maintenance mode enabled.', true); | ||||||
|  |         } elseif ($action === 'maintenance_off') { | ||||||
|  |             require_once __DIR__ . '/../core/Maintenance.php'; | ||||||
|  |             \App\Core\Maintenance::disable(); | ||||||
|  |             Feedback::flash('NOTICE', 'DEFAULT', 'Maintenance mode disabled.', true); | ||||||
|  |         } elseif ($action === 'migrate_up') { | ||||||
|  |             require_once __DIR__ . '/../core/MigrationRunner.php'; | ||||||
|  |             $migrationsDir = __DIR__ . '/../../doc/database/migrations'; | ||||||
|  |             $runner = new \App\Core\MigrationRunner($db, $migrationsDir); | ||||||
|  |             $applied = $runner->applyPendingMigrations(); | ||||||
|  |             if (empty($applied)) { | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', 'No pending migrations.', true); | ||||||
|  |             } else { | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', 'Applied migrations: ' . implode(', ', $applied), true); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } catch (Throwable $e) { | ||||||
|  |         Feedback::flash('ERROR', 'DEFAULT', 'Action failed: ' . $e->getMessage(), false); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     header('Location: ' . $app_root . '?page=admin-tools'); | ||||||
|  |     exit; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Prepare data for view
 | ||||||
|  | require_once __DIR__ . '/../core/Maintenance.php'; | ||||||
|  | $maintenance_enabled = \App\Core\Maintenance::isEnabled(); | ||||||
|  | $maintenance_message = \App\Core\Maintenance::getMessage(); | ||||||
|  | 
 | ||||||
|  | require_once __DIR__ . '/../core/MigrationRunner.php'; | ||||||
|  | $migrationsDir = __DIR__ . '/../../doc/database/migrations'; | ||||||
|  | $pending = []; | ||||||
|  | $applied = []; | ||||||
|  | $migration_contents = []; | ||||||
|  | try { | ||||||
|  |     $runner = new \App\Core\MigrationRunner($db, $migrationsDir); | ||||||
|  |     $pending = $runner->listPendingMigrations(); | ||||||
|  |     $applied = $runner->listAppliedMigrations(); | ||||||
|  |     // Preload contents for billing-admin style modals
 | ||||||
|  |     $all = array_unique(array_merge($pending, $applied)); | ||||||
|  |     foreach ($all as $fname) { | ||||||
|  |         $path = realpath($migrationsDir . '/' . $fname); | ||||||
|  |         if ($path && strpos($path, realpath($migrationsDir)) === 0) { | ||||||
|  |             $content = @file_get_contents($path); | ||||||
|  |             if ($content !== false) { | ||||||
|  |                 $migration_contents[$fname] = $content; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } catch (Throwable $e) { | ||||||
|  |     // show error in the page
 | ||||||
|  |     $migration_error = $e->getMessage(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CSRF token
 | ||||||
|  | $csrf_token = $security->generateCsrfToken(); | ||||||
|  | 
 | ||||||
|  | // Load the template
 | ||||||
|  | include __DIR__ . '/../templates/admin-tools.php'; | ||||||
|  | @ -8,61 +8,167 @@ | ||||||
|  * to allow time-based invalidation if needed. |  * to allow time-based invalidation if needed. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| // Get any new messages
 | // Constants for session keys and cache settings
 | ||||||
| include '../app/includes/messages.php'; | define('SESSION_CACHE_SUFFIX', '_cache'); | ||||||
| include '../app/includes/messages-show.php'; | define('SESSION_CACHE_TIME_SUFFIX', '_cache_time'); | ||||||
|  | define('CACHE_EXPIRY_TIME', 3600); // 1 hour in seconds
 | ||||||
|  | 
 | ||||||
|  | // Input validation
 | ||||||
|  | $action = isset($_GET['action']) ? htmlspecialchars(trim($_GET['action']), ENT_QUOTES, 'UTF-8') : ''; | ||||||
|  | $agentId = filter_input(INPUT_GET, 'agent', FILTER_VALIDATE_INT); | ||||||
| 
 | 
 | ||||||
| $action = $_REQUEST['action'] ?? ''; |  | ||||||
| $agent = $_REQUEST['agent'] ?? ''; |  | ||||||
| require '../app/classes/agent.php'; | require '../app/classes/agent.php'; | ||||||
|  | require '../app/classes/host.php'; | ||||||
|  | $agentObject = new Agent($db); | ||||||
|  | $hostObject = new Host($db); | ||||||
| 
 | 
 | ||||||
| $agentObject = new Agent($dbWeb); | /** | ||||||
| 
 |  * Get the cache key for an agent | ||||||
| // if it's a POST request, it's saving to cache
 |  * @param int $agentId The agent ID | ||||||
| if ($_SERVER['REQUEST_METHOD'] == 'POST') { |  * @param string $suffix The suffix to append (_cache or _cache_time) | ||||||
| 
 |  * @return string The cache key | ||||||
|     // read the JSON sent from javascript
 |  */ | ||||||
|     $data = file_get_contents("php://input"); | function getAgentCacheKey($agentId, $suffix) { | ||||||
|     $result = json_decode($data, true); |     return "agent{$agentId}{$suffix}"; | ||||||
| 
 |  | ||||||
|     // store the data in the session
 |  | ||||||
|     if ($result) { |  | ||||||
|         $_SESSION["agent{$agent}_cache"] = $result; |  | ||||||
|         $_SESSION["agent{$agent}_cache_time"] = time();  // store the cache time
 |  | ||||||
|         echo json_encode([ |  | ||||||
|             'status'    => 'success', |  | ||||||
|             'message'   => "Cache for agent {$agent} is stored." |  | ||||||
|         ]); |  | ||||||
|     } elseif ($result === null && !empty($agent)) { |  | ||||||
|         unset($_SESSION["agent{$agent}_cache"]); |  | ||||||
|         unset($_SESSION["agent{$agent}_cache_time"]); |  | ||||||
|         echo json_encode([ |  | ||||||
|             'status'    => 'success', |  | ||||||
|             'message'   => "Cache for agent {$agent} is cleared." |  | ||||||
|         ]); |  | ||||||
|     } else { |  | ||||||
|         echo json_encode([ |  | ||||||
|             'status'    => 'error', |  | ||||||
|             'message'   => 'Invalid data' |  | ||||||
|         ]); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| //// if it's a GET request, it's read/load from cache
 |  | ||||||
| //} elseif ($loadcache === true) {
 |  | ||||||
| //
 |  | ||||||
| //    // check if cached data exists in session
 |  | ||||||
| //    if (isset($_SESSION["agent{$agent}_cache"])) {
 |  | ||||||
| //        // return the cached data in JSON format
 |  | ||||||
| //        echo json_encode(['status' => 'success', 'data' => $_SESSION["agent{$agent}_cache"]]);
 |  | ||||||
| //    } else {
 |  | ||||||
| //        // if no cached data exists
 |  | ||||||
| //        echo json_encode(['status' => 'error', 'message' => 'No cached data found']);
 |  | ||||||
| //    }
 |  | ||||||
| 
 |  | ||||||
| // no form submitted, show the templates
 |  | ||||||
| } else { |  | ||||||
|     $agentDetails = $agentObject->getAgentDetails($platform_id); |  | ||||||
|     include '../app/templates/agent-list.php'; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| ?>
 | /** | ||||||
|  |  * Check if cache is expired | ||||||
|  |  * @param int $agentId The agent ID | ||||||
|  |  * @return bool True if cache is expired or doesn't exist | ||||||
|  |  */ | ||||||
|  | function isCacheExpired($agentId) { | ||||||
|  |     $timeKey = getAgentCacheKey($agentId, SESSION_CACHE_TIME_SUFFIX); | ||||||
|  |     if (!isset($_SESSION[$timeKey])) { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  |     return (time() - $_SESSION[$timeKey]) > CACHE_EXPIRY_TIME; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Handle POST request (saving to cache)
 | ||||||
|  | if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||||||
|  | 
 | ||||||
|  |     // Apply rate limiting for adding new contacts
 | ||||||
|  |     require '../app/includes/rate_limit_middleware.php'; | ||||||
|  |     checkRateLimit($db, 'contact', $userId); | ||||||
|  | 
 | ||||||
|  |     // Validate agent ID for POST operations
 | ||||||
|  |     if ($agentId === false || $agentId === null) { | ||||||
|  |         Feedback::flash('ERROR', 'DEFAULT', 'Invalid agent ID format'); | ||||||
|  |         echo json_encode(['status' => 'error', 'message' => 'Invalid agent ID format']); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Read and validate JSON data
 | ||||||
|  |     $jsonData = file_get_contents("php://input"); | ||||||
|  |     if ($jsonData === false) { | ||||||
|  |         Feedback::flash('ERROR', 'DEFAULT', 'Failed to read input data'); | ||||||
|  |         echo json_encode(['status' => 'error', 'message' => 'Failed to read input data']); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     $data = json_decode($jsonData, true); | ||||||
|  | 
 | ||||||
|  |     // Handle cache clearing
 | ||||||
|  |     if ($data === null && !empty($agentId)) { | ||||||
|  |         $cacheKey = getAgentCacheKey($agentId, SESSION_CACHE_SUFFIX); | ||||||
|  |         $timeKey = getAgentCacheKey($agentId, SESSION_CACHE_TIME_SUFFIX); | ||||||
|  | 
 | ||||||
|  |         unset($_SESSION[$cacheKey]); | ||||||
|  |         unset($_SESSION[$timeKey]); | ||||||
|  | 
 | ||||||
|  |         Feedback::flash('SUCCESS', 'DEFAULT', "Cache for agent {$agentId} is cleared."); | ||||||
|  |         echo json_encode([ | ||||||
|  |             'status' => 'success', | ||||||
|  |             'message' => "Cache for agent {$agentId} is cleared." | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  |     // Handle cache storing
 | ||||||
|  |     elseif ($data) { | ||||||
|  |         $cacheKey = getAgentCacheKey($agentId, SESSION_CACHE_SUFFIX); | ||||||
|  |         $timeKey = getAgentCacheKey($agentId, SESSION_CACHE_TIME_SUFFIX); | ||||||
|  | 
 | ||||||
|  |         $_SESSION[$cacheKey] = $data; | ||||||
|  |         $_SESSION[$timeKey] = time(); | ||||||
|  | 
 | ||||||
|  |         Feedback::flash('SUCCESS', 'DEFAULT', "Cache for agent {$agentId} is stored."); | ||||||
|  |         echo json_encode([ | ||||||
|  |             'status' => 'success', | ||||||
|  |             'message' => "Cache for agent {$agentId} is stored." | ||||||
|  |         ]); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         Feedback::flash('ERROR', 'DEFAULT', 'Invalid data format'); | ||||||
|  |         echo json_encode(['status' => 'error', 'message' => 'Invalid data format']); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | // Handle AJAX requests
 | ||||||
|  | } elseif (isset($_GET['action'])) { | ||||||
|  |     $action = $_GET['action']; | ||||||
|  |     $agentId = filter_input(INPUT_GET, 'agent', FILTER_VALIDATE_INT); | ||||||
|  | 
 | ||||||
|  |     if ($action === 'fetch') { | ||||||
|  |         $response = ['status' => 'success', 'data' => $data]; | ||||||
|  |         echo json_encode($response); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ($action === 'status') { | ||||||
|  |         $response = ['status' => 'success', 'data' => $statusData]; | ||||||
|  |         echo json_encode($response); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | // Handle template display
 | ||||||
|  | } else { | ||||||
|  | 
 | ||||||
|  |     // Validate platform_id is set
 | ||||||
|  |     if (!isset($platform_id)) { | ||||||
|  |         Feedback::flash('ERROR', 'DEFAULT', 'Platform ID is not set'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Get host details for this platform
 | ||||||
|  |     $hostDetails = $hostObject->getHostDetails($platform_id); | ||||||
|  | 
 | ||||||
|  |     // Group agents by host
 | ||||||
|  |     $agentsByHost = []; | ||||||
|  |     foreach ($hostDetails as $host) { | ||||||
|  |         $hostId = $host['id']; | ||||||
|  |         $agentsByHost[$hostId] = [ | ||||||
|  |             'host_name' => $host['name'], | ||||||
|  |             'agents' => [] | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         // Get agents for this host
 | ||||||
|  |         $hostAgents = $agentObject->getAgentDetails($hostId); | ||||||
|  |         if ($hostAgents) { | ||||||
|  |             $agentsByHost[$hostId]['agents'] = $hostAgents; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Generate JWT tokens for each agent beforehand
 | ||||||
|  |         $agentTokens = []; | ||||||
|  |         foreach ($agentsByHost[$hostId]['agents'] as $agent) { | ||||||
|  |             $payload = [ | ||||||
|  |                 'iss' => 'Jilo Web', | ||||||
|  |                 'aud' => $config['domain'], | ||||||
|  |                 'iat' => time(), | ||||||
|  |                 'exp' => time() + 3600, | ||||||
|  |                 'agent_id' => $agent['id'] | ||||||
|  |             ]; | ||||||
|  |             $agentTokens[$agent['id']] = $agentObject->generateAgentToken($payload, $agent['secret_key']); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Now we have: | ||||||
|  |          * $hostDetails - hosts in this platform | ||||||
|  |          * $agentsByHost[$hostId]['agents'] - agents details by hostId | ||||||
|  |          * $agentTokens[$agent['id']] - tokens for the agentsIds | ||||||
|  |          */ | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Get any new feedback messages
 | ||||||
|  |     include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  |     // Load the template
 | ||||||
|  |     include '../app/templates/agents.php'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -8,26 +8,46 @@ | ||||||
|  * Supports pagination. |  * Supports pagination. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| // Get any new messages
 |  | ||||||
| include '../app/includes/messages.php'; |  | ||||||
| include '../app/includes/messages-show.php'; |  | ||||||
| 
 |  | ||||||
| require '../app/classes/component.php'; |  | ||||||
| 
 |  | ||||||
| // connect to database
 | // connect to database
 | ||||||
| $response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id); | $response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id); | ||||||
| 
 | 
 | ||||||
| // if DB connection has error, display it and stop here
 | // if DB connection has error, display it and stop here
 | ||||||
| if ($response['db'] === null) { | if ($response['db'] === null) { | ||||||
|     Messages::flash('ERROR', 'DEFAULT', $response['error']); |     Feedback::flash('ERROR', 'DEFAULT', $response['error']); | ||||||
| 
 | 
 | ||||||
| // otherwise if DB connection is OK, go on
 | // otherwise if DB connection is OK, go on
 | ||||||
| } else { | } else { | ||||||
|     $db = $response['db']; |     $db = $response['db']; | ||||||
| 
 | 
 | ||||||
|  |     // Get current page for pagination
 | ||||||
|  |     $currentPage = $_REQUEST['page_num'] ?? 1; | ||||||
|  |     $currentPage = (int)$currentPage; | ||||||
|  | 
 | ||||||
|     // specify time range
 |     // specify time range
 | ||||||
|     include '../app/helpers/time_range.php'; |     include '../app/helpers/time_range.php'; | ||||||
| 
 | 
 | ||||||
|  |     // Build params for pagination
 | ||||||
|  |     $params = ''; | ||||||
|  |     if (!empty($_REQUEST['from_time'])) { | ||||||
|  |         $params .= '&from_time=' . urlencode($_REQUEST['from_time']); | ||||||
|  |     } | ||||||
|  |     if (!empty($_REQUEST['until_time'])) { | ||||||
|  |         $params .= '&until_time=' . urlencode($_REQUEST['until_time']); | ||||||
|  |     } | ||||||
|  |     if (!empty($_REQUEST['name'])) { | ||||||
|  |         $params .= '&name=' . urlencode($_REQUEST['name']); | ||||||
|  |     } | ||||||
|  |     if (!empty($_REQUEST['id'])) { | ||||||
|  |         $params .= '&id=' . urlencode($_REQUEST['id']); | ||||||
|  |     } | ||||||
|  |     if (isset($_REQUEST['event'])) { | ||||||
|  |         $params .= '&event=' . urlencode($_REQUEST['event']); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // pagination variables
 | ||||||
|  |     $items_per_page = 20; | ||||||
|  |     $offset = ($currentPage -1) * $items_per_page; | ||||||
|  | 
 | ||||||
|     // jitsi component events list
 |     // jitsi component events list
 | ||||||
|     // we use $_REQUEST, so that both links and forms work
 |     // we use $_REQUEST, so that both links and forms work
 | ||||||
|     // if it's there, but empty, we make it same as the field name; otherwise assign the value
 |     // if it's there, but empty, we make it same as the field name; otherwise assign the value
 | ||||||
|  | @ -40,15 +60,9 @@ if ($response['db'] === null) { | ||||||
|     // Component events listings
 |     // Component events listings
 | ||||||
|     //
 |     //
 | ||||||
| 
 | 
 | ||||||
| 
 |     require '../app/classes/component.php'; | ||||||
|     // list of all component events (default)
 |  | ||||||
|     $componentObject = new Component($db); |     $componentObject = new Component($db); | ||||||
| 
 | 
 | ||||||
|     // pagination variables
 |  | ||||||
|     $items_per_page = 15; |  | ||||||
|     $browse_page = $_REQUEST['p'] ?? 1; |  | ||||||
|     $browse_page = (int)$browse_page; |  | ||||||
|     $offset = ($browse_page -1) * $items_per_page; |  | ||||||
| 
 | 
 | ||||||
|     // prepare the result
 |     // prepare the result
 | ||||||
|     $search = $componentObject->jitsiComponents($jitsi_component, $component_id, $event_type, $from_time, $until_time, $offset, $items_per_page); |     $search = $componentObject->jitsiComponents($jitsi_component, $component_id, $event_type, $from_time, $until_time, $offset, $items_per_page); | ||||||
|  | @ -57,7 +71,7 @@ if ($response['db'] === null) { | ||||||
|     if (!empty($search)) { |     if (!empty($search)) { | ||||||
|         // we get total items and number of pages
 |         // we get total items and number of pages
 | ||||||
|         $item_count = count($search_all); |         $item_count = count($search_all); | ||||||
|         $page_count = ceil($item_count / $items_per_page); |         $totalPages = ceil($item_count / $items_per_page); | ||||||
| 
 | 
 | ||||||
|         $components = array(); |         $components = array(); | ||||||
|         $components['records'] = array(); |         $components['records'] = array(); | ||||||
|  | @ -78,30 +92,18 @@ if ($response['db'] === null) { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // prepare the widget
 |     // filter message
 | ||||||
|     $widget['full'] = false; |     $filterMessage = array(); | ||||||
|     $widget['name'] = 'AllComponents'; |  | ||||||
|     $widget['filter'] = true; |  | ||||||
|     $widget['pagination'] = true; |  | ||||||
| 
 |  | ||||||
|     // widget title
 |  | ||||||
|     if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') { |     if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') { | ||||||
|         $widget['title'] = 'Jitsi events for component <strong>' . $_REQUEST['name'] . '</strong>'; |         array_push($filterMessage, 'Jitsi events for component "<strong>' . $_REQUEST['name'] . '</strong>"'); | ||||||
|     } elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') { |     } elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') { | ||||||
|         $widget['title'] = 'Jitsi events for component ID <strong>' . $_REQUEST['id'] . '</strong>'; |         array_push($filterMessage, 'Jitsi events for component ID "<strong>' . $_REQUEST['id'] . '</strong>"'); | ||||||
|     } else { |  | ||||||
|         $widget['title'] = 'Jitsi events for <strong>all components</strong>'; |  | ||||||
|     } |  | ||||||
|     // widget records
 |  | ||||||
|     if (!empty($components['records'])) { |  | ||||||
|         $widget['full'] = true; |  | ||||||
|         $widget['table_headers'] = array_keys($components['records'][0]); |  | ||||||
|         $widget['table_records'] = $components['records']; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // Get any new feedback messages
 | ||||||
|  |     include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|     // display the widget
 |     // display the widget
 | ||||||
|     include '../app/templates/event-list-components.php'; |     include '../app/templates/components.php'; | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -8,18 +8,12 @@ | ||||||
|  * Supports pagination. |  * Supports pagination. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| // Get any new messages
 |  | ||||||
| include '../app/includes/messages.php'; |  | ||||||
| include '../app/includes/messages-show.php'; |  | ||||||
| 
 |  | ||||||
| require '../app/classes/conference.php'; |  | ||||||
| 
 |  | ||||||
| // connect to database
 | // connect to database
 | ||||||
| $response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id); | $response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id); | ||||||
| 
 | 
 | ||||||
| // if DB connection has error, display it and stop here
 | // if DB connection has error, display it and stop here
 | ||||||
| if ($response['db'] === null) { | if ($response['db'] === null) { | ||||||
|     Messages::flash('ERROR', 'DEFAULT', $response['error']); |     Feedback::flash('ERROR', 'DEFAULT', $response['error']); | ||||||
| 
 | 
 | ||||||
| // otherwise if DB connection is OK, go on
 | // otherwise if DB connection is OK, go on
 | ||||||
| } else { | } else { | ||||||
|  | @ -52,13 +46,31 @@ if ($response['db'] === null) { | ||||||
|     // Conference listings
 |     // Conference listings
 | ||||||
|     //
 |     //
 | ||||||
| 
 | 
 | ||||||
|  |     require '../app/classes/conference.php'; | ||||||
|     $conferenceObject = new Conference($db); |     $conferenceObject = new Conference($db); | ||||||
| 
 | 
 | ||||||
|  |     // get current page for pagination
 | ||||||
|  |     $currentPage = $_REQUEST['page_num'] ?? 1; | ||||||
|  |     $currentPage = (int)$currentPage; | ||||||
|  | 
 | ||||||
|     // pagination variables
 |     // pagination variables
 | ||||||
|     $items_per_page = 15; |     $items_per_page = 20; | ||||||
|     $browse_page = $_REQUEST['p'] ?? 1; |     $offset = ($currentPage -1) * $items_per_page; | ||||||
|     $browse_page = (int)$browse_page; | 
 | ||||||
|     $offset = ($browse_page -1) * $items_per_page; |     // Build params for pagination
 | ||||||
|  |     $params = ''; | ||||||
|  |     if (!empty($_REQUEST['from_time'])) { | ||||||
|  |         $params .= '&from_time=' . urlencode($_REQUEST['from_time']); | ||||||
|  |     } | ||||||
|  |     if (!empty($_REQUEST['until_time'])) { | ||||||
|  |         $params .= '&until_time=' . urlencode($_REQUEST['until_time']); | ||||||
|  |     } | ||||||
|  |     if (!empty($_REQUEST['name'])) { | ||||||
|  |         $params .= '&name=' . urlencode($_REQUEST['name']); | ||||||
|  |     } | ||||||
|  |     if (!empty($_REQUEST['id'])) { | ||||||
|  |         $params .= '&id=' . urlencode($_REQUEST['id']); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     // search and list specific conference ID
 |     // search and list specific conference ID
 | ||||||
|     if (isset($conferenceId)) { |     if (isset($conferenceId)) { | ||||||
|  | @ -77,7 +89,7 @@ if ($response['db'] === null) { | ||||||
|     if (!empty($search)) { |     if (!empty($search)) { | ||||||
|         // we get total items and number of pages
 |         // we get total items and number of pages
 | ||||||
|         $item_count = count($search_all); |         $item_count = count($search_all); | ||||||
|         $page_count = ceil($item_count / $items_per_page); |         $totalPages = ceil($item_count / $items_per_page); | ||||||
| 
 | 
 | ||||||
|         $conferences = array(); |         $conferences = array(); | ||||||
|         $conferences['records'] = array(); |         $conferences['records'] = array(); | ||||||
|  | @ -139,32 +151,18 @@ if ($response['db'] === null) { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // prepare the widget
 |     // filter message
 | ||||||
|     $widget['full'] = false; |     $filterMessage = array(); | ||||||
|     $widget['name'] = 'Conferences'; |  | ||||||
|     $widget['collapsible'] = false; |  | ||||||
|     $widget['collapsed'] = false; |  | ||||||
|     $widget['filter'] = true; |  | ||||||
|     $widget['pagination'] = true; |  | ||||||
| 
 |  | ||||||
|     // widget title
 |  | ||||||
|     if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') { |     if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') { | ||||||
|         $widget['title'] = 'Conferences with name matching "<strong>' . $_REQUEST['name'] . '"</strong>'; |         array_push($filterMessage, 'Conferences with name matching "<strong>' . $_REQUEST['name'] . '</strong>"'); | ||||||
|     } elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') { |     } elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') { | ||||||
|         $widget['title'] = 'Conference with ID "<strong>' . $_REQUEST['id'] . '"</strong>'; |         array_push($filterMessage, 'Conference with ID "<strong>' . $_REQUEST['id'] . '</strong>"'); | ||||||
|     } else { |  | ||||||
|         $widget['title'] = 'All conferences'; |  | ||||||
|     } |  | ||||||
|     // widget records
 |  | ||||||
|     if (!empty($conferences['records'])) { |  | ||||||
|         $widget['full'] = true; |  | ||||||
|         $widget['table_headers'] = array_keys($conferences['records'][0]); |  | ||||||
|         $widget['table_records'] = $conferences['records']; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // Get any new feedback messages
 | ||||||
|  |     include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|     // display the widget
 |     // display the widget
 | ||||||
|     include '../app/templates/event-list-conferences.php'; |     include '../app/templates/conferences.php'; | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -1,257 +1,124 @@ | ||||||
| <?php | <?php | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Configuration management. |  * Config management. | ||||||
|  * |  * | ||||||
|  * This page ("config") handles configuration by adding, editing, and deleting platforms, |  * This page handles the config file. | ||||||
|  * hosts, agents, and the configuration file itself. |  | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| // Get any new messages
 | // Get any new feedback messages
 | ||||||
| include '../app/includes/messages.php'; | include '../app/helpers/feedback.php'; | ||||||
| include '../app/includes/messages-show.php'; |  | ||||||
| 
 |  | ||||||
| $action = $_REQUEST['action'] ?? ''; |  | ||||||
| $agent = $_REQUEST['agent'] ?? ''; |  | ||||||
| $host = $_REQUEST['host'] ?? ''; |  | ||||||
| 
 | 
 | ||||||
| require '../app/classes/config.php'; | require '../app/classes/config.php'; | ||||||
| require '../app/classes/host.php'; | require '../app/classes/api_response.php'; | ||||||
| require '../app/classes/agent.php'; |  | ||||||
| 
 | 
 | ||||||
|  | // Initialize required objects
 | ||||||
|  | $userObject = new User($db); | ||||||
| $configObject = new Config(); | $configObject = new Config(); | ||||||
| $hostObject = new Host($dbWeb); |  | ||||||
| $agentObject = new Agent($dbWeb); |  | ||||||
| 
 | 
 | ||||||
| if ($_SERVER['REQUEST_METHOD'] == 'POST') { | // For AJAX requests
 | ||||||
|     /** | $isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && | ||||||
|      * Handles form submissions from editing page |           strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; | ||||||
|      */ |  | ||||||
| 
 | 
 | ||||||
|     // editing the config file
 | // Set JSON content type for AJAX requests
 | ||||||
|     if (isset($_POST['item']) && $_POST['item'] === 'config_file') { | if ($isAjax) { | ||||||
|         // check if file is writable
 |     header('Content-Type: application/json'); | ||||||
|         if (!is_writable($config_file)) { | } | ||||||
|             $_SESSION['error'] = "Configuration file is not writable."; |  | ||||||
|         } else { |  | ||||||
|             $result = $configObject->editConfigFile($_POST, $config_file); |  | ||||||
|             if ($result === true) { |  | ||||||
|                 $_SESSION['notice'] = "The config file is edited."; |  | ||||||
|             } else { |  | ||||||
|                 $_SESSION['error'] = "Editing the config file failed. Error: $result"; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|     // new host adding
 | // Ensure config file path is set
 | ||||||
|     } elseif (isset($_POST['new']) && isset($_POST['item']) && $_POST['new'] === 'true' && $_POST['item'] === 'host') { | if (!isset($config_file) || empty($config_file)) { | ||||||
|         $newHost = [ |     if ($isAjax) { | ||||||
|             'address'       => $address, |         ApiResponse::error('Config file path not set'); | ||||||
|             'port'          => $port, |         exit; | ||||||
|             'platform_id'   => $platform_id, |  | ||||||
|             'name'          => $name, |  | ||||||
|         ]; |  | ||||||
|         $result = $hostObject->addHost($newHost); |  | ||||||
|         if ($result === true) { |  | ||||||
|             $_SESSION['notice'] = "New Jilo host added."; |  | ||||||
|         } else { |  | ||||||
|             $_SESSION['error'] = "Adding the host failed. Error: $result"; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     // new agent adding
 |  | ||||||
|     } elseif (isset($_POST['new']) && isset($_POST['item']) && $_POST['new'] === 'true' && $_POST['item'] === 'agent') { |  | ||||||
|         $newAgent = [ |  | ||||||
|             'type_id'       => $type, |  | ||||||
|             'url'           => $url, |  | ||||||
|             'secret_key'	=> $secret_key, |  | ||||||
|             'check_period'  => $check_period, |  | ||||||
|         ]; |  | ||||||
|         $result = $agentObject->addAgent($platform_id, $newAgent); |  | ||||||
|         if ($result === true) { |  | ||||||
|             $_SESSION['notice'] = "New Jilo Agent added."; |  | ||||||
|         } else { |  | ||||||
|             $_SESSION['error'] = "Adding the agent failed. Error: $result"; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     // new platform adding
 |  | ||||||
|     } elseif (isset($_POST['new']) && $_POST['new'] === 'true') { |  | ||||||
|         $newPlatform = [ |  | ||||||
|             'name'          => $name, |  | ||||||
|             'jitsi_url'		=> $_POST['jitsi_url'], |  | ||||||
|             'jilo_database'	=> $_POST['jilo_database'], |  | ||||||
|         ]; |  | ||||||
|         $result = $platformObject->addPlatform($newPlatform); |  | ||||||
|         if ($result === true) { |  | ||||||
|             $_SESSION['notice'] = "New Jitsi platform added."; |  | ||||||
|         } else { |  | ||||||
|             $_SESSION['error'] = "Adding the platform failed. Error: $result"; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     // deleting a host
 |  | ||||||
|     } elseif (isset($_POST['delete']) && isset($_POST['host']) && $_POST['delete'] === 'true') { |  | ||||||
|         $result = $hostObject->deleteHost($host); |  | ||||||
|         if ($result === true) { |  | ||||||
|             $_SESSION['notice'] = "Host id \"{$_REQUEST['host']}\" deleted."; |  | ||||||
|         } else { |  | ||||||
|             $_SESSION['error'] = "Deleting the host failed. Error: $result"; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     // deleting an agent
 |  | ||||||
|     } elseif (isset($_POST['delete']) && isset($_POST['agent']) && $_POST['delete'] === 'true') { |  | ||||||
|         $result = $agentObject->deleteAgent($agent); |  | ||||||
|         if ($result === true) { |  | ||||||
|             $_SESSION['notice'] = "Agent id \"{$_REQUEST['agent']}\" deleted."; |  | ||||||
|         } else { |  | ||||||
|             $_SESSION['error'] = "Deleting the agent failed. Error: $result"; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     // deleting a platform
 |  | ||||||
|     } elseif (isset($_POST['delete']) && $_POST['delete'] === 'true') { |  | ||||||
|         $platform = $_POST['platform']; |  | ||||||
|         $result = $platformObject->deletePlatform($platform); |  | ||||||
|         if ($result === true) { |  | ||||||
|             $_SESSION['notice'] = "Platform \"{$platformObject['name']}\" added."; |  | ||||||
|         } else { |  | ||||||
|             $_SESSION['error'] = "Adding the platform failed. Error: $result"; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     // an update to an existing host
 |  | ||||||
|     } elseif (isset($_POST['host'])) { |  | ||||||
|         $updatedHost = [ |  | ||||||
|             'id'        => $host, |  | ||||||
|             'address'   => $address, |  | ||||||
|             'port'      => $port, |  | ||||||
|             'name'      => $name, |  | ||||||
|         ]; |  | ||||||
|         $result = $hostObject->editHost($platform_id, $updatedHost); |  | ||||||
|         if ($result === true) { |  | ||||||
|             $_SESSION['notice'] = "Host \"{$_REQUEST['address']}:{$_REQUEST['port']}\" edited."; |  | ||||||
|         } else { |  | ||||||
|             $_SESSION['error'] = "Editing the host failed. Error: $result"; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     // an update to an existing agent
 |  | ||||||
|     } elseif (isset($_POST['agent'])) { |  | ||||||
|         $updatedAgent = [ |  | ||||||
|             'id'            => $agent, |  | ||||||
|             'agent_type_id' => $type, |  | ||||||
|             'url'           => $url, |  | ||||||
|             'secret_key'	=> $secret_key, |  | ||||||
|             'check_period'  => $check_period, |  | ||||||
|         ]; |  | ||||||
|         $result = $agentObject->editAgent($platform_id, $updatedAgent); |  | ||||||
|         if ($result === true) { |  | ||||||
|             $_SESSION['notice'] = "Agent id \"{$_REQUEST['agent']}\" edited."; |  | ||||||
|         } else { |  | ||||||
|             $_SESSION['error'] = "Editing the agent failed. Error: $result"; |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     // an update to an existing platform
 |  | ||||||
|     } else { |     } else { | ||||||
|         $platform = $_POST['platform']; |         Feedback::flash('ERROR', 'DEFAULT', 'Config file path not set'); | ||||||
|         $updatedPlatform = [ |         header('Location: ' . htmlspecialchars($app_root)); | ||||||
|             'name'		    => $name, |         exit; | ||||||
|             'jitsi_url'		=> $_POST['jitsi_url'], |     } | ||||||
|             'jilo_database'	=> $_POST['jilo_database'], | } | ||||||
|         ]; |  | ||||||
|         $result = $platformObject->editPlatform($platform, $updatedPlatform); |  | ||||||
|         if ($result === true) { |  | ||||||
|             $_SESSION['notice'] = "Platform \"{$_REQUEST['name']}\" edited."; |  | ||||||
|         } else { |  | ||||||
|             $_SESSION['error'] = "Editing the platform failed. Error: $result"; |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|  | // Check if file is writable
 | ||||||
|  | $isWritable = is_writable($config_file); | ||||||
|  | $configMessage = ''; | ||||||
|  | if (!$isWritable) { | ||||||
|  |     $configMessage = Feedback::render('ERROR', 'DEFAULT', 'Config file is not writable', false); | ||||||
|  |     if ($isAjax) { | ||||||
|  |         ApiResponse::error('Config file is not writable', null, 403); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||||||
|  |     // Check if user has permission to edit config
 | ||||||
|  |     if (!$userObject->hasRight($userId, 'edit config file')) { | ||||||
|  |         $logObject->log('error', "Unauthorized: User \"$currentUser\" tried to edit config file. IP: $user_IP", ['user_id' => $userId, 'scope' => 'system']);
 | ||||||
|  |         if ($isAjax) { | ||||||
|  |             ApiResponse::error('Forbidden: You do not have permission to edit the config file', null, 403); | ||||||
|  |             exit; | ||||||
|  |         } else { | ||||||
|  |             include '../app/templates/error-unauthorized.php'; | ||||||
|  |             exit; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| // FIXME the new file is not loaded on first page load
 |     // Apply rate limiting
 | ||||||
|     unset($config); |     require_once '../app/includes/rate_limit_middleware.php'; | ||||||
|     header("Location: $app_root?page=config&item=$item"); |     checkRateLimit($db, 'config', $userId); | ||||||
|     exit(); |  | ||||||
| 
 | 
 | ||||||
| } else { |     // Ensure no output before this point
 | ||||||
|  |     ob_clean(); | ||||||
|  | 
 | ||||||
|  |     // For AJAX requests, get JSON data
 | ||||||
|  |     if ($isAjax) { | ||||||
|  |         // Get raw input
 | ||||||
|  |         $jsonData = file_get_contents('php://input'); | ||||||
|  |         if ($jsonData === false) { | ||||||
|  |             $logObject->log('error', "Failed to read request data for config update", ['user_id' => $userId, 'scope' => 'system']); | ||||||
|  |             ApiResponse::error('Failed to read request data'); | ||||||
|  |             exit; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Try to parse JSON
 | ||||||
|  |         $postData = json_decode($jsonData, true); | ||||||
|  |         if (json_last_error() !== JSON_ERROR_NONE) { | ||||||
|  |             $error = json_last_error_msg(); | ||||||
|  |             ApiResponse::error('Invalid JSON data received: ' . $error); | ||||||
|  |             exit; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Try to update config file
 | ||||||
|  |         $result = $configObject->editConfigFile($postData, $config_file); | ||||||
|  |         if ($result['success']) { | ||||||
|  |             ApiResponse::success($result['updated'], 'Config file updated successfully'); | ||||||
|  |         } else { | ||||||
|  |             ApiResponse::error($result['error']); | ||||||
|  |         } | ||||||
|  |         exit; | ||||||
|  |     } else { | ||||||
|  |         // Handle non-AJAX POST
 | ||||||
|  |         $result = $configObject->editConfigFile($_POST, $config_file); | ||||||
|  |         if ($result['success']) { | ||||||
|  |             Feedback::flash('NOTICE', 'DEFAULT', 'Config file updated successfully', true); | ||||||
|  |         } else { | ||||||
|  |             Feedback::flash('ERROR', 'DEFAULT', "Error updating config file: " . $result['error'], true); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         header('Location: ' . htmlspecialchars($app_root) . '?page=config'); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Only include template for non-AJAX requests
 | ||||||
|  | if (!$isAjax) { | ||||||
|     /** |     /** | ||||||
|      * Handles GET requests to display templates. |      * Handles GET requests to display templates. | ||||||
|      */ |      */ | ||||||
| 
 | 
 | ||||||
|     switch ($item) { |     if ($userObject->hasRight($userId, 'superuser') || | ||||||
| 
 |       $userObject->hasRight($userId, 'view config file')) { | ||||||
|         case 'platform': |         include '../app/templates/config.php'; | ||||||
|             if (isset($action) && $action === 'add') { |     } else { | ||||||
|                 include '../app/templates/config-platform-add.php'; |         $logObject->log('error', "Unauthorized: User \"$currentUser\" tried to access \"config\" page. IP: $user_IP", ['user_id' => $userId, 'scope' => 'system']); | ||||||
|             } elseif (isset($action) && $action === 'edit') { |         include '../app/templates/error-unauthorized.php'; | ||||||
|                 include '../app/templates/config-platform-edit.php'; |  | ||||||
|             } elseif (isset($action) && $action === 'delete') { |  | ||||||
|                 include '../app/templates/config-platform-delete.php'; |  | ||||||
|             } else { |  | ||||||
|                 if ($userObject->hasRight($user_id, 'view config file')) { |  | ||||||
|                     include '../app/templates/config-platform.php'; |  | ||||||
|                 } else { |  | ||||||
|                     include '../app/templates/error-unauthorized.php'; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             break; |  | ||||||
| 
 |  | ||||||
|         case 'host': |  | ||||||
|             if (isset($action) && $action === 'add') { |  | ||||||
|                 include '../app/templates/config-host-add.php'; |  | ||||||
|             } elseif (isset($action) && $action === 'edit') { |  | ||||||
|                 $hostDetails = $hostObject->getHostDetails($platform_id, $agent); |  | ||||||
|                 include '../app/templates/config-host-edit.php'; |  | ||||||
|             } elseif (isset($action) && $action === 'delete') { |  | ||||||
|                 $hostDetails = $hostObject->getHostDetails($platform_id, $agent); |  | ||||||
|                 include '../app/templates/config-host-delete.php'; |  | ||||||
|             } else { |  | ||||||
|                 if ($userObject->hasRight($user_id, 'view config file')) { |  | ||||||
|                     $hostDetails = $hostObject->getHostDetails(); |  | ||||||
|                     include '../app/templates/config-host.php'; |  | ||||||
|                 } else { |  | ||||||
|                     include '../app/templates/error-unauthorized.php'; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             break; |  | ||||||
| 
 |  | ||||||
|         case 'endpoint': |  | ||||||
|             // TODO ad here endpoints options
 |  | ||||||
|             echo 'under construction'; |  | ||||||
| //            switch ($action) {
 |  | ||||||
| //                case 'add-agent':
 |  | ||||||
| //                    $jilo_agent_types = $agentObject->getAgentTypes();
 |  | ||||||
| //                    $jilo_agents_in_platform = $agentObject->getPlatformAgentTypes($platform_id);
 |  | ||||||
| //                    $jilo_agent_types_in_platform = array_column($jilo_agents_in_platform, 'agent_type_id');
 |  | ||||||
| //                    include '../app/templates/config-add-agent.php';
 |  | ||||||
| //                    break;
 |  | ||||||
| //                case 'edit':
 |  | ||||||
| //                    if (isset($_GET['agent'])) {
 |  | ||||||
| //                        $agentDetails = $agentObject->getAgentDetails($platform_id, $agent);
 |  | ||||||
| //                        $jilo_agent_types = $agentObject->getAgentTypes();
 |  | ||||||
| //                        include '../app/templates/config-edit-agent.php';
 |  | ||||||
| //                    }
 |  | ||||||
| //                    break;
 |  | ||||||
| //                case 'delete':
 |  | ||||||
| //                    if (isset($_GET['agent'])) {
 |  | ||||||
| //                        $agentDetails = $agentObject->getAgentDetails($platform_id, $agent);
 |  | ||||||
| //                        include '../app/templates/config-delete-agent.php';
 |  | ||||||
| //                    }
 |  | ||||||
| //                    break;
 |  | ||||||
| //            }
 |  | ||||||
|             break; |  | ||||||
| 
 |  | ||||||
|         case 'config_file': |  | ||||||
|             if (isset($action) && $action === 'edit') { |  | ||||||
|                 include '../app/templates/config-configfile-edit.php'; |  | ||||||
|             } else { |  | ||||||
|                 if ($userObject->hasRight($user_id, 'view config file')) { |  | ||||||
|                     include '../app/templates/config-configfile.php'; |  | ||||||
|                 } else { |  | ||||||
|                     include '../app/templates/error-unauthorized.php'; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             break; |  | ||||||
| 
 |  | ||||||
|         default: |  | ||||||
|         // the default config page is the platforms page
 |  | ||||||
|             header("Location: $app_root?page=config&item=platform"); |  | ||||||
|             exit(); |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,167 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * User credentials management | ||||||
|  |  * | ||||||
|  |  * This page ("credentials") handles all credential-related actions including: | ||||||
|  |  * - Two-factor authentication (2FA) setup, verification, and management | ||||||
|  |  * - Password changes and resets | ||||||
|  |  * | ||||||
|  |  * Actions handled: | ||||||
|  |  * - `setup`: Initial 2FA setup and verification | ||||||
|  |  * - `verify`: Verify 2FA codes during login | ||||||
|  |  * - `disable`: Disable 2FA | ||||||
|  |  * - `password`: Change password | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | // Initialize user object
 | ||||||
|  | $userObject = new User($db); | ||||||
|  | 
 | ||||||
|  | // Get action and item from request
 | ||||||
|  | $action = $_REQUEST['action'] ?? ''; | ||||||
|  | $item = $_REQUEST['item'] ?? ''; | ||||||
|  | 
 | ||||||
|  | // if a form is submitted
 | ||||||
|  | if ($_SERVER['REQUEST_METHOD'] == 'POST') { | ||||||
|  |     // Validate CSRF token
 | ||||||
|  |     $security->verifyCsrfToken($_POST['csrf_token'] ?? ''); | ||||||
|  |     if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) { | ||||||
|  |         Feedback::flash('ERROR', 'DEFAULT', 'Invalid security token. Please try again.'); | ||||||
|  |         header("Location: $app_root?page=credentials"); | ||||||
|  |         exit(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Apply rate limiting
 | ||||||
|  |     require_once '../app/includes/rate_limit_middleware.php'; | ||||||
|  |     checkRateLimit($db, 'credentials', $userId); | ||||||
|  | 
 | ||||||
|  |     switch ($item) { | ||||||
|  |         case '2fa': | ||||||
|  |             switch ($action) { | ||||||
|  |                 case 'setup': | ||||||
|  |                     // Validate the setup code
 | ||||||
|  |                     $code = $_POST['code'] ?? ''; | ||||||
|  |                     $secret = $_POST['secret'] ?? ''; | ||||||
|  | 
 | ||||||
|  |                     if ($userObject->enableTwoFactor($userId, $secret, $code)) { | ||||||
|  |                         Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been enabled successfully.'); | ||||||
|  |                         header("Location: $app_root?page=credentials"); | ||||||
|  |                         exit(); | ||||||
|  |                     } else { | ||||||
|  |                         // Only show error if code was actually submitted
 | ||||||
|  |                         if ($code !== '') { | ||||||
|  |                             Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code. Please try again.'); | ||||||
|  |                         } | ||||||
|  |                         header("Location: $app_root?page=credentials&action=setup"); | ||||||
|  |                         exit(); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  | 
 | ||||||
|  |                 case 'verify': | ||||||
|  |                     // This is a user-initiated verification
 | ||||||
|  |                     $code = $_POST['code'] ?? ''; | ||||||
|  |                     if ($userObject->verifyTwoFactor($userId, $code)) { | ||||||
|  |                         $_SESSION['2fa_verified'] = true; | ||||||
|  |                         header("Location: $app_root?page=dashboard"); | ||||||
|  |                         exit(); | ||||||
|  |                     } else { | ||||||
|  |                         Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code. Please try again.'); | ||||||
|  |                         header("Location: $app_root?page=credentials&action=verify"); | ||||||
|  |                         exit(); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  | 
 | ||||||
|  |                 case 'disable': | ||||||
|  |                     if ($userObject->disableTwoFactor($userId)) { | ||||||
|  |                         Feedback::flash('NOTICE', 'DEFAULT', 'Two-factor authentication has been disabled.'); | ||||||
|  |                     } else { | ||||||
|  |                         Feedback::flash('ERROR', 'DEFAULT', 'Failed to disable two-factor authentication.'); | ||||||
|  |                     } | ||||||
|  |                     header("Location: $app_root?page=credentials"); | ||||||
|  |                     exit(); | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  | 
 | ||||||
|  |         case 'password': | ||||||
|  |             require_once '../app/classes/validator.php'; | ||||||
|  | 
 | ||||||
|  |             $validator = new Validator($_POST); | ||||||
|  |             $rules = [ | ||||||
|  |                 'current_password' => [ | ||||||
|  |                     'required' => true | ||||||
|  |                 ], | ||||||
|  |                 'new_password' => [ | ||||||
|  |                     'required' => true, | ||||||
|  |                     'min' => 8 | ||||||
|  |                 ], | ||||||
|  |                 'confirm_password' => [ | ||||||
|  |                     'required' => true, | ||||||
|  |                     'matches' => 'new_password' | ||||||
|  |                 ] | ||||||
|  |             ]; | ||||||
|  | 
 | ||||||
|  |             if (!$validator->validate($rules)) { | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError()); | ||||||
|  |                 header("Location: $app_root?page=credentials"); | ||||||
|  |                 exit(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if ($userObject->changePassword($userId, $_POST['current_password'], $_POST['new_password'])) { | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', 'Password has been changed successfully.'); | ||||||
|  |             } else { | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', 'Failed to change password. Please verify your current password.'); | ||||||
|  |             } | ||||||
|  |             header("Location: $app_root?page=credentials"); | ||||||
|  |             exit(); | ||||||
|  |             break; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | // no form submitted, show the templates
 | ||||||
|  | } else { | ||||||
|  |     // Get user timezone for templates
 | ||||||
|  |     $userTimezone = !empty($userDetails[0]['timezone']) ? $userDetails[0]['timezone'] : 'UTC'; | ||||||
|  | 
 | ||||||
|  |     // Generate CSRF token if not exists
 | ||||||
|  |     require_once '../app/helpers/security.php'; | ||||||
|  |     $security = SecurityHelper::getInstance(); | ||||||
|  |     $security->generateCsrfToken(); | ||||||
|  | 
 | ||||||
|  |     // Get 2FA status for the template
 | ||||||
|  |     $has2fa = $userObject->isTwoFactorEnabled($userId); | ||||||
|  | 
 | ||||||
|  |     switch ($action) { | ||||||
|  |         case 'setup': | ||||||
|  |             if (!$has2fa) { | ||||||
|  |                 $result = $userObject->enableTwoFactor($userId); | ||||||
|  |                 if ($result['success']) { | ||||||
|  |                     $setupData = $result['data']; | ||||||
|  |                 } else { | ||||||
|  |                     Feedback::flash('ERROR', 'DEFAULT', $result['message'] ?? 'Failed to generate 2FA setup data'); | ||||||
|  |                     header("Location: $app_root?page=credentials"); | ||||||
|  |                     exit(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             // Get any new feedback messages
 | ||||||
|  |             include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  |             // Load the 2FA setup template
 | ||||||
|  |             include '../app/templates/credentials-2fa-setup.php'; | ||||||
|  |             break; | ||||||
|  | 
 | ||||||
|  |         case 'verify': | ||||||
|  |             // Get any new feedback messages
 | ||||||
|  |             include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  |             // Load the 2FA verification template
 | ||||||
|  |             include '../app/templates/credentials-2fa-verify.php'; | ||||||
|  |             break; | ||||||
|  | 
 | ||||||
|  |         default: | ||||||
|  |             // Get any new feedback messages
 | ||||||
|  |             include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  |             // Load the combined management template
 | ||||||
|  |             include '../app/templates/credentials-manage.php'; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -9,19 +9,18 @@ | ||||||
|  * 3. The most recent 10 conferences. |  * 3. The most recent 10 conferences. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| // Get any new messages
 | // Get any new feedback messages
 | ||||||
| include '../app/includes/messages.php'; | include '../app/helpers/feedback.php'; | ||||||
| include '../app/includes/messages-show.php'; |  | ||||||
| 
 | 
 | ||||||
| require '../app/classes/conference.php'; | require '../app/classes/conference.php'; | ||||||
| require '../app/classes/participant.php'; | require '../app/classes/participant.php'; | ||||||
| 
 | 
 | ||||||
| // connect to database
 | // connect to database
 | ||||||
| $response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id); | $response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id); | ||||||
| 
 | 
 | ||||||
| // if DB connection has error, display it and stop here
 | // if DB connection has error, display it and stop here
 | ||||||
| if ($response['db'] === null) { | if ($response['db'] === null) { | ||||||
|     Messages::flash('ERROR', 'DEFAULT', $response['error']); |     Feedback::flash('ERROR', 'DEFAULT', $response['error']); | ||||||
| 
 | 
 | ||||||
| // otherwise if DB connection is OK, go on
 | // otherwise if DB connection is OK, go on
 | ||||||
| } else { | } else { | ||||||
|  | @ -228,5 +227,3 @@ if ($response['db'] === null) { | ||||||
|     include '../app/templates/widget.php'; |     include '../app/templates/widget.php'; | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -1,207 +0,0 @@ | ||||||
| <?php |  | ||||||
| 
 |  | ||||||
| // Get any new messages
 |  | ||||||
| include '../app/includes/messages.php'; |  | ||||||
| include '../app/includes/messages-show.php'; |  | ||||||
| 
 |  | ||||||
| $action = $_REQUEST['action'] ?? ''; |  | ||||||
| $agent = $_REQUEST['agent'] ?? ''; |  | ||||||
| 
 |  | ||||||
| require '../app/classes/config.php'; |  | ||||||
| require '../app/classes/agent.php'; |  | ||||||
| require '../app/classes/conference.php'; |  | ||||||
| 
 |  | ||||||
| $configObject = new Config(); |  | ||||||
| $agentObject = new Agent($dbWeb); |  | ||||||
| 
 |  | ||||||
| // connect to Jilo database
 |  | ||||||
| $response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id); |  | ||||||
| 
 |  | ||||||
| // if DB connection has error, display it and stop here
 |  | ||||||
| if ($response['db'] === null) { |  | ||||||
|     Messages::flash('ERROR', 'DEFAULT', $response['error']); |  | ||||||
| 
 |  | ||||||
| // otherwise if DB connection is OK, go on
 |  | ||||||
| } else { |  | ||||||
|     $db = $response['db']; |  | ||||||
| 
 |  | ||||||
|     $conferenceObject = new Conference($db); |  | ||||||
| 
 |  | ||||||
|     switch ($item) { |  | ||||||
| 
 |  | ||||||
|         case 'graphs': |  | ||||||
|             // Connect to Jilo database for log data
 |  | ||||||
|             $jilo_response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id); |  | ||||||
|             if ($jilo_response['db'] === null) { |  | ||||||
|                 Messages::flash('ERROR', 'DEFAULT', $jilo_response['error']); |  | ||||||
|                 break; |  | ||||||
|             } |  | ||||||
|             $jilo_db = $jilo_response['db']; |  | ||||||
| 
 |  | ||||||
|             // Get date range for the last 7 days
 |  | ||||||
|             $from_time = date('Y-m-d', strtotime('-7 days')); |  | ||||||
|             $until_time = date('Y-m-d'); |  | ||||||
| 
 |  | ||||||
|             // Define graphs to show
 |  | ||||||
|             $graphs = [ |  | ||||||
|                 [ |  | ||||||
|                     'graph_name' => 'conferences', |  | ||||||
|                     'graph_title' => 'Conferences in "' . htmlspecialchars($platformDetails[0]['name']) . '" over time', |  | ||||||
|                     'datasets' => [] |  | ||||||
|                 ], |  | ||||||
|                 [ |  | ||||||
|                     'graph_name' => 'participants', |  | ||||||
|                     'graph_title' => 'Participants in "' . htmlspecialchars($platformDetails[0]['name']) . '" over time', |  | ||||||
|                     'datasets' => [] |  | ||||||
|                 ] |  | ||||||
|             ]; |  | ||||||
| 
 |  | ||||||
|             // Get Jitsi API data
 |  | ||||||
|             $conferences_api = $agentObject->getHistoricalData( |  | ||||||
|                 $platform_id,  |  | ||||||
|                 'jicofo', |  | ||||||
|                 'conferences', |  | ||||||
|                 $from_time, |  | ||||||
|                 $until_time |  | ||||||
|             ); |  | ||||||
|             $graphs[0]['datasets'][] = [ |  | ||||||
|                 'data' => $conferences_api, |  | ||||||
|                 'label' => 'Conferences from Jitsi API', |  | ||||||
|                 'color' => 'rgba(75, 192, 192, 1)' |  | ||||||
|             ]; |  | ||||||
| 
 |  | ||||||
|             // Get conference data from logs
 |  | ||||||
|             $conferences_logs = $conferenceObject->conferenceNumber( |  | ||||||
|                 $from_time, |  | ||||||
|                 $until_time |  | ||||||
|             ); |  | ||||||
|             $graphs[0]['datasets'][] = [ |  | ||||||
|                 'data' => $conferences_logs, |  | ||||||
|                 'label' => 'Conferences from Logs', |  | ||||||
|                 'color' => 'rgba(255, 99, 132, 1)' |  | ||||||
|             ]; |  | ||||||
| 
 |  | ||||||
|             // Get participants data
 |  | ||||||
|             $participants_api = $agentObject->getHistoricalData( |  | ||||||
|                 $platform_id,  |  | ||||||
|                 'jicofo', |  | ||||||
|                 'participants', |  | ||||||
|                 $from_time, |  | ||||||
|                 $until_time |  | ||||||
|             ); |  | ||||||
|             $graphs[1]['datasets'][] = [ |  | ||||||
|                 'data' => $participants_api, |  | ||||||
|                 'label' => 'Participants from Jitsi API', |  | ||||||
|                 'color' => 'rgba(75, 192, 192, 1)' |  | ||||||
|             ]; |  | ||||||
| 
 |  | ||||||
|             // Prepare data for template
 |  | ||||||
|             $graph = $graphs; |  | ||||||
| 
 |  | ||||||
|             // prepare the widget
 |  | ||||||
|             $widget['full'] = false; |  | ||||||
|             $widget['name'] = 'Graphs'; |  | ||||||
|             $widget['title'] = 'Jitsi graphs'; |  | ||||||
| 
 |  | ||||||
|             include '../app/templates/graphs-combined.php'; |  | ||||||
|             break; |  | ||||||
| 
 |  | ||||||
|         case 'latest': |  | ||||||
|             // Define metrics to display
 |  | ||||||
|             $metrics = [ |  | ||||||
|                 'Basic stats' => [ |  | ||||||
|                     'conferences' => ['label' => 'Current conferences', 'link' => 'conferences'], |  | ||||||
|                     'participants' => ['label' => 'Current participants', 'link' => 'participants'], |  | ||||||
|                     'total_conferences_created' => ['label' => 'Total conferences created'], |  | ||||||
|                     'total_participants' => ['label' => 'Total participants'] |  | ||||||
|                 ], |  | ||||||
|                 'Bridge stats' => [ |  | ||||||
|                     'bridge_selector.bridge_count' => ['label' => 'Bridge count'], |  | ||||||
|                     'bridge_selector.operational_bridge_count' => ['label' => 'Operational bridges'], |  | ||||||
|                     'bridge_selector.in_shutdown_bridge_count' => ['label' => 'Bridges in shutdown'] |  | ||||||
|                 ], |  | ||||||
|                 'Jibri stats' => [ |  | ||||||
|                     'jibri_detector.count' => ['label' => 'Jibri count'], |  | ||||||
|                     'jibri_detector.available' => ['label' => 'Jibri idle'], |  | ||||||
|                     'jibri.live_streaming_active' => ['label' => 'Jibri active streaming'], |  | ||||||
|                     'jibri.recording_active' => ['label' => 'Jibri active recording'], |  | ||||||
| 
 |  | ||||||
|                 ], |  | ||||||
|                 'System stats' => [ |  | ||||||
|                     'threads' => ['label' => 'Threads'], |  | ||||||
|                     'stress_level' => ['label' => 'Stress level'], |  | ||||||
|                     'version' => ['label' => 'Version'] |  | ||||||
|                 ] |  | ||||||
|             ]; |  | ||||||
| 
 |  | ||||||
|             // Get latest data for all the agents
 |  | ||||||
|             $agents = ['jvb', 'jicofo', 'jibri', 'prosody', 'nginx']; |  | ||||||
|             $widget['records'] = []; |  | ||||||
| 
 |  | ||||||
|             // Initialize records for each agent
 |  | ||||||
|             foreach ($agents as $agent) { |  | ||||||
|                 $record = [ |  | ||||||
|                     'table_headers' => strtoupper($agent), |  | ||||||
|                     'metrics' => [], |  | ||||||
|                     'timestamp' => null |  | ||||||
|                 ]; |  | ||||||
| 
 |  | ||||||
|                 // Fetch all metrics for this agent
 |  | ||||||
|                 foreach ($metrics as $section => $section_metrics) { |  | ||||||
|                     foreach ($section_metrics as $metric => $metricConfig) { |  | ||||||
|                         $data = $agentObject->getLatestData($platform_id, $agent, $metric); |  | ||||||
|                         if ($data !== null) { |  | ||||||
|                             $record['metrics'][$section][$metric] = [ |  | ||||||
|                                 'value' => $data['value'], |  | ||||||
|                                 'label' => $metricConfig['label'], |  | ||||||
|                                 'link' => isset($metricConfig['link']) ? $metricConfig['link'] : null |  | ||||||
|                             ]; |  | ||||||
|                             // Use the most recent timestamp
 |  | ||||||
|                             if ($record['timestamp'] === null || strtotime($data['timestamp']) > strtotime($record['timestamp'])) { |  | ||||||
|                                 $record['timestamp'] = $data['timestamp']; |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (!empty($record['metrics'])) { |  | ||||||
|                     $widget['records'][] = $record; |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             // prepare the widget
 |  | ||||||
|             $widget['full'] = false; |  | ||||||
|             $widget['name'] = 'LatestData'; |  | ||||||
|             $widget['title'] = 'Latest data from Jilo Agents'; |  | ||||||
|             $widget['collapsible'] = false; |  | ||||||
|             $widget['collapsed'] = false; |  | ||||||
|             $widget['filter'] = false; |  | ||||||
|             $widget['metrics'] = $metrics; // Pass metrics configuration to template
 |  | ||||||
|             if (!empty($widget['records'])) { |  | ||||||
|                 $widget['full'] = true; |  | ||||||
|             } |  | ||||||
|             $widget['pagination'] = false; |  | ||||||
| 
 |  | ||||||
|             include '../app/templates/latest-data.php'; |  | ||||||
|             break; |  | ||||||
| 
 |  | ||||||
|         case 'configjs': |  | ||||||
|             $mode = $_REQUEST['mode'] ?? ''; |  | ||||||
|             $raw = ($mode === 'raw'); |  | ||||||
|             $platformConfigjs = $configObject->getPlatformConfigjs($platformDetails[0]['jitsi_url'], $raw); |  | ||||||
|             include '../app/templates/data-configjs.php'; |  | ||||||
|             break; |  | ||||||
| 
 |  | ||||||
|         case 'interfaceconfigjs': |  | ||||||
|             $mode = $_REQUEST['mode'] ?? ''; |  | ||||||
|             $raw = ($mode === 'raw'); |  | ||||||
|             $platformInterfaceConfigjs = $configObject->getPlatformInterfaceConfigjs($platformDetails[0]['jitsi_url'], $raw); |  | ||||||
|             include '../app/templates/data-interfaceconfigjs.php'; |  | ||||||
|             break; |  | ||||||
| 
 |  | ||||||
|         default: |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  | @ -0,0 +1,91 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | $action = $_REQUEST['action'] ?? ''; | ||||||
|  | $agent = $_REQUEST['agent'] ?? ''; | ||||||
|  | 
 | ||||||
|  | require '../app/classes/agent.php'; | ||||||
|  | require '../app/classes/conference.php'; | ||||||
|  | require '../app/classes/host.php'; | ||||||
|  | 
 | ||||||
|  | $agentObject = new Agent($db); | ||||||
|  | $hostObject = new Host($db); | ||||||
|  | 
 | ||||||
|  | // Connect to Jilo database for log data
 | ||||||
|  | $response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id); | ||||||
|  | if ($response['db'] === null) { | ||||||
|  |     Feedback::flash('ERROR', 'DEFAULT', $response['error']); | ||||||
|  | } else { | ||||||
|  |     $db = $response['db']; | ||||||
|  | } | ||||||
|  | $conferenceObject = new Conference($db); | ||||||
|  | 
 | ||||||
|  | // Get date range for the last 7 days
 | ||||||
|  | $from_time = date('Y-m-d', strtotime('-7 days')); | ||||||
|  | $until_time = date('Y-m-d'); | ||||||
|  | 
 | ||||||
|  | // Define graphs to show
 | ||||||
|  | $graphs = [ | ||||||
|  |     [ | ||||||
|  |         'graph_name' => 'conferences', | ||||||
|  |         'graph_title' => 'Conferences in "' . htmlspecialchars($platformDetails[0]['name']) . '" over time', | ||||||
|  |         'datasets' => [] | ||||||
|  |     ], | ||||||
|  |     [ | ||||||
|  |         'graph_name' => 'participants', | ||||||
|  |         'graph_title' => 'Participants in "' . htmlspecialchars($platformDetails[0]['name']) . '" over time', | ||||||
|  |         'datasets' => [] | ||||||
|  |     ] | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | // Get Jitsi API data
 | ||||||
|  | $conferences_api = $agentObject->getHistoricalData( | ||||||
|  |     $platform_id,  | ||||||
|  |     'jicofo', | ||||||
|  |     'conferences', | ||||||
|  |     $from_time, | ||||||
|  |     $until_time | ||||||
|  | ); | ||||||
|  | $graphs[0]['datasets'][] = [ | ||||||
|  |     'data' => $conferences_api, | ||||||
|  |     'label' => 'Conferences from Jitsi API', | ||||||
|  |     'color' => 'rgba(75, 192, 192, 1)' | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | // Get conference data from logs
 | ||||||
|  | $conferences_logs = $conferenceObject->conferenceNumber( | ||||||
|  |     $from_time, | ||||||
|  |     $until_time | ||||||
|  | ); | ||||||
|  | $graphs[0]['datasets'][] = [ | ||||||
|  |     'data' => $conferences_logs, | ||||||
|  |     'label' => 'Conferences from Logs', | ||||||
|  |     'color' => 'rgba(255, 99, 132, 1)' | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | // Get participants data
 | ||||||
|  | $participants_api = $agentObject->getHistoricalData( | ||||||
|  |     $platform_id,  | ||||||
|  |     'jicofo', | ||||||
|  |     'participants', | ||||||
|  |     $from_time, | ||||||
|  |     $until_time | ||||||
|  | ); | ||||||
|  | $graphs[1]['datasets'][] = [ | ||||||
|  |     'data' => $participants_api, | ||||||
|  |     'label' => 'Participants from Jitsi API', | ||||||
|  |     'color' => 'rgba(75, 192, 192, 1)' | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | // Prepare data for template
 | ||||||
|  | $graph = $graphs; | ||||||
|  | 
 | ||||||
|  | // prepare the widget
 | ||||||
|  | $widget['full'] = false; | ||||||
|  | $widget['name'] = 'Graphs'; | ||||||
|  | $widget['title'] = 'Jitsi graphs'; | ||||||
|  | 
 | ||||||
|  | // Get any new feedback messages
 | ||||||
|  | include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  | // Load the template
 | ||||||
|  | include '../app/templates/graphs.php'; | ||||||
|  | @ -1,9 +1,6 @@ | ||||||
| <?php | <?php | ||||||
| 
 | 
 | ||||||
| // Get any new messages
 | // Get any new feedback messages
 | ||||||
| include '../app/includes/messages.php'; | include '../app/helpers/feedback.php'; | ||||||
| include '../app/includes/messages-show.php'; |  | ||||||
| 
 | 
 | ||||||
| include '../app/templates/help-main.php'; | include '../app/templates/help.php'; | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,106 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | require '../app/classes/agent.php'; | ||||||
|  | require '../app/classes/host.php'; | ||||||
|  | 
 | ||||||
|  | $agentObject = new Agent($db); | ||||||
|  | $hostObject = new Host($db); | ||||||
|  | 
 | ||||||
|  | // Define metrics to display
 | ||||||
|  | $metrics = [ | ||||||
|  |     'Basic stats' => [ | ||||||
|  |         'conferences' => ['label' => 'Current conferences', 'link' => 'conferences'], | ||||||
|  |         'participants' => ['label' => 'Current participants', 'link' => 'participants'], | ||||||
|  |         'total_conferences_created' => ['label' => 'Total conferences created'], | ||||||
|  |         'total_participants' => ['label' => 'Total participants'] | ||||||
|  |     ], | ||||||
|  |     'Bridge stats' => [ | ||||||
|  |         'bridge_selector.bridge_count' => ['label' => 'Bridge count'], | ||||||
|  |         'bridge_selector.operational_bridge_count' => ['label' => 'Operational bridges'], | ||||||
|  |         'bridge_selector.in_shutdown_bridge_count' => ['label' => 'Bridges in shutdown'] | ||||||
|  |     ], | ||||||
|  |     'Jibri stats' => [ | ||||||
|  |         'jibri_detector.count' => ['label' => 'Jibri count'], | ||||||
|  |         'jibri_detector.available' => ['label' => 'Jibri idle'], | ||||||
|  |         'jibri.live_streaming_active' => ['label' => 'Jibri active streaming'], | ||||||
|  |         'jibri.recording_active' => ['label' => 'Jibri active recording'], | ||||||
|  |     ], | ||||||
|  |     'System stats' => [ | ||||||
|  |         'threads' => ['label' => 'Threads'], | ||||||
|  |         'stress_level' => ['label' => 'Stress level'], | ||||||
|  |         'version' => ['label' => 'Version'] | ||||||
|  |     ] | ||||||
|  | ]; | ||||||
|  | 
 | ||||||
|  | // Get all hosts for this platform
 | ||||||
|  | $hosts = $hostObject->getHostDetails($platform_id); | ||||||
|  | $hostsData = []; | ||||||
|  | 
 | ||||||
|  | // For each host, get its agents and their metrics
 | ||||||
|  | foreach ($hosts as $host) { | ||||||
|  |     $hostData = [ | ||||||
|  |         'id' => $host['id'], | ||||||
|  |         'name' => $host['name'] ?: $host['address'], | ||||||
|  |         'address' => $host['address'], | ||||||
|  |         'agents' => [] | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     // Get agents for this host
 | ||||||
|  |     $hostAgents = $agentObject->getAgentDetails($host['id']); | ||||||
|  |     foreach ($hostAgents as $agent) { | ||||||
|  |         $agentData = [ | ||||||
|  |             'id' => $agent['id'], | ||||||
|  |             'type' => $agent['agent_description'], | ||||||
|  |             'name' => strtoupper($agent['agent_description']), | ||||||
|  |             'metrics' => [], | ||||||
|  |             'timestamp' => null | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         // Fetch all metrics for this agent
 | ||||||
|  |         foreach ($metrics as $section => $section_metrics) { | ||||||
|  |             foreach ($section_metrics as $metric => $metricConfig) { | ||||||
|  |                 // Get latest data
 | ||||||
|  |                 $latestData = $agentObject->getLatestData($host['id'], $agent['agent_description'], $metric); | ||||||
|  | 
 | ||||||
|  |                 if ($latestData !== null) { | ||||||
|  |                     // Get the previous record
 | ||||||
|  |                     $previousData = $agentObject->getPreviousRecord( | ||||||
|  |                         $host['id'],  | ||||||
|  |                         $agent['agent_description'],  | ||||||
|  |                         $metric, | ||||||
|  |                         $latestData['timestamp'] | ||||||
|  |                     ); | ||||||
|  | 
 | ||||||
|  |                     $agentData['metrics'][$section][$metric] = [ | ||||||
|  |                         'latest' => [ | ||||||
|  |                             'value' => $latestData['value'], | ||||||
|  |                             'timestamp' => $latestData['timestamp'] | ||||||
|  |                         ], | ||||||
|  |                         'previous' => $previousData, | ||||||
|  |                         'label' => $metricConfig['label'], | ||||||
|  |                         'link' => isset($metricConfig['link']) ? $metricConfig['link'] : null | ||||||
|  |                     ]; | ||||||
|  | 
 | ||||||
|  |                     // Use the most recent timestamp for the agent
 | ||||||
|  |                     if ($agentData['timestamp'] === null || strtotime($latestData['timestamp']) > strtotime($agentData['timestamp'])) { | ||||||
|  |                         $agentData['timestamp'] = $latestData['timestamp']; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!empty($agentData['metrics'])) { | ||||||
|  |             $hostData['agents'][] = $agentData; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!empty($hostData['agents'])) { | ||||||
|  |         $hostsData[] = $hostData; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Get any new feedback messages
 | ||||||
|  | include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  | // Load the template
 | ||||||
|  | include '../app/templates/latest.php'; | ||||||
|  | @ -0,0 +1,16 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | $mode = $_REQUEST['mode'] ?? ''; | ||||||
|  | $raw = ($mode === 'raw'); | ||||||
|  | $livejsFile = $_REQUEST['item'] ?? ''; | ||||||
|  | 
 | ||||||
|  | require '../app/classes/settings.php'; | ||||||
|  | $settingsObject = new Settings(); | ||||||
|  | 
 | ||||||
|  | $livejsData = $settingsObject->getPlatformJsFile($platformDetails[0]['jitsi_url'], $item, $raw); | ||||||
|  | 
 | ||||||
|  | // Get any new feedback messages
 | ||||||
|  | include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  | // Load the template
 | ||||||
|  | include '../app/templates/livejs.php'; | ||||||
|  | @ -4,105 +4,302 @@ | ||||||
|  * User login |  * User login | ||||||
|  * |  * | ||||||
|  * This page ("login") handles user login, session management, cookie handling, and error logging. |  * This page ("login") handles user login, session management, cookie handling, and error logging. | ||||||
|  * Supports "remember me" functionality to extend session duration. |  * Supports "remember me" functionality to extend session duration and two-factor authentication. | ||||||
|  * |  * | ||||||
|  * Actions Performed: |  * Actions Performed: | ||||||
|  * - Validates login credentials. |  * - Validates login credentials | ||||||
|  * - Manages session and cookies based on "remember me" option. |  * - Handles two-factor authentication if enabled | ||||||
|  * - Logs successful and failed login attempts. |  * - Manages session and cookies based on "remember me" option | ||||||
|  * - Displays login form and optional custom messages. |  * - Logs successful and failed login attempts | ||||||
|  |  * - Displays login form and optional custom messages | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| // clear the global error var before login
 | // clear the global error var before login
 | ||||||
| unset($error); | unset($error); | ||||||
| 
 | 
 | ||||||
| try { | try { | ||||||
| 
 |  | ||||||
|     // connect to database
 |     // connect to database
 | ||||||
|     $dbWeb = connectDB($config); |     $db = connectDB($config); | ||||||
| 
 | 
 | ||||||
|     // Initialize RateLimiter
 |     // Initialize RateLimiter
 | ||||||
|     require_once '../app/classes/ratelimiter.php'; |     require_once '../app/classes/ratelimiter.php'; | ||||||
|     $rateLimiter = new RateLimiter($dbWeb['db']); |     $rateLimiter = new RateLimiter($db); | ||||||
|  |     // Get user IP
 | ||||||
|  |     require_once '../app/helpers/ip_helper.php'; | ||||||
|  |     $user_IP = getUserIP(); | ||||||
| 
 | 
 | ||||||
|     if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) { |     $action = $_REQUEST['action'] ?? ''; | ||||||
|         try { |  | ||||||
|             $username = $_POST['username']; |  | ||||||
|             $password = $_POST['password']; |  | ||||||
| 
 | 
 | ||||||
|             // Check if IP is blacklisted
 |     if ($action === 'verify' && isset($_SESSION['2fa_pending_user_id'])) { | ||||||
|             if ($rateLimiter->isIpBlacklisted($user_IP)) { |         // Handle 2FA verification
 | ||||||
|                 throw new Exception(Messages::get('LOGIN', 'IP_BLACKLISTED')['message']); |         $code = $_POST['code'] ?? ''; | ||||||
|             } |         $pending2FA = Session::get2FAPending(); | ||||||
| 
 | 
 | ||||||
|             // Check rate limiting (but skip if IP is whitelisted)
 |         if (!$pending2FA) { | ||||||
|             if (!$rateLimiter->isIpWhitelisted($user_IP)) { |             header('Location: ' . htmlspecialchars($app_root) . '?page=login'); | ||||||
|                 $attempts = $rateLimiter->getRecentAttempts($user_IP); |             exit(); | ||||||
|                 if ($attempts >= $rateLimiter->maxAttempts) { |         } | ||||||
|                     throw new Exception(Messages::get('LOGIN', 'LOGIN_BLOCKED')['message']); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             // login successful
 |         require_once '../app/classes/twoFactorAuth.php'; | ||||||
|             if ( $userObject->login($username, $password) ) { |         $twoFactorAuth = new TwoFactorAuthentication($db); | ||||||
|                 // if remember_me is checked, max out the session
 | 
 | ||||||
|                 if (isset($_POST['remember_me'])) { |         if ($twoFactorAuth->verify($pending2FA['user_id'], $code)) { | ||||||
|                     // 30*24*60*60 = 30 days
 |             // Complete login
 | ||||||
|                     $cookie_lifetime = 30 * 24 * 60 * 60; |             handleSuccessfulLogin($pending2FA['user_id'], $pending2FA['username'], | ||||||
|                     $setcookie_lifetime = time() + 30 * 24 * 60 * 60; |                 $pending2FA['remember_me'], $config, $app_root, $logObject, $user_IP); | ||||||
|                     $gc_maxlifetime = 30 * 24 * 60 * 60; | 
 | ||||||
|                 } else { |             // Clean up 2FA session data
 | ||||||
|                     // 0 - session end on browser close
 |             Session::clear2FAPending(); | ||||||
|                     // 1440 - 24 minutes (default)
 | 
 | ||||||
|                     $cookie_lifetime = 0; |             exit(); | ||||||
|                     $setcookie_lifetime = 0; |         } | ||||||
|                     $gc_maxlifetime = 1440; | 
 | ||||||
|  |         // If we get here (and we have code submitted), verification failed
 | ||||||
|  |         if (!empty($code)) { | ||||||
|  |             Feedback::flash('ERROR', 'DEFAULT', 'Invalid verification code'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Get any new feedback messages
 | ||||||
|  |         include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  |         // Make userId available to template
 | ||||||
|  |         $userId = $pending2FA['user_id']; | ||||||
|  | 
 | ||||||
|  |         // Load the 2FA verification template
 | ||||||
|  |         include '../app/templates/credentials-2fa-verify.php'; | ||||||
|  |         exit(); | ||||||
|  |     } elseif ($action === 'forgot') { | ||||||
|  |         if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||||||
|  |             // Handle password reset request
 | ||||||
|  |             try { | ||||||
|  |                 // Validate CSRF token
 | ||||||
|  |                 $security = SecurityHelper::getInstance(); | ||||||
|  |                 if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) { | ||||||
|  |                     throw new Exception('Invalid security token. Please try again.'); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // set session lifetime and cookies
 |                 // Apply rate limiting
 | ||||||
|                 setcookie('username', $username, [ |                 if (!$rateLimiter->isIpWhitelisted($user_IP)) { | ||||||
|                     'expires'	=> $setcookie_lifetime, |                     if ($rateLimiter->isIpBlacklisted($user_IP)) { | ||||||
|                     'path'		=> $config['folder'], |                         throw new Exception(Feedback::get('LOGIN', 'IP_BLACKLISTED')['message']); | ||||||
|                     'domain'	=> $config['domain'], |                     } | ||||||
|                     'secure'	=> isset($_SERVER['HTTPS']), |                     if ($rateLimiter->tooManyAttempts('password_reset', $user_IP)) { | ||||||
|                     'httponly'	=> true, |                         throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']); | ||||||
|                     'samesite'	=> 'Strict' |                     } | ||||||
|                 ]); |                     $rateLimiter->attempt('password_reset', $user_IP); | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 // Log successful login
 |                 // Validate email
 | ||||||
|                 $user_id = $userObject->getUserId($username)[0]['id']; |                 $email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL); | ||||||
|                 $logObject->insertLog($user_id, "Login: User \"$username\" logged in. IP: $user_IP", 'user');
 |                 if (!$email) { | ||||||
|  |                     throw new Exception('Please enter a valid email address.'); | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 // Set success message and redirect
 |                 // Process reset request
 | ||||||
|                 Messages::flash('LOGIN', 'LOGIN_SUCCESS', null, true); |                 require_once '../app/classes/passwordReset.php'; | ||||||
|                 header('Location: ' . htmlspecialchars($app_root)); |                 $resetHandler = new PasswordReset($db, $config); | ||||||
|  |                 $result = $resetHandler->requestReset($email); | ||||||
|  | 
 | ||||||
|  |                 // Always show same message whether email exists or not for security
 | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', $result['message']); | ||||||
|  |                 header("Location: $app_root?page=login"); | ||||||
|                 exit(); |                 exit(); | ||||||
|             } else { | 
 | ||||||
|                 throw new Exception(Messages::get('LOGIN', 'LOGIN_FAILED')['message']); |             } catch (Exception $e) { | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', $e->getMessage()); | ||||||
|             } |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Generate CSRF token
 | ||||||
|  |         $security = SecurityHelper::getInstance(); | ||||||
|  |         $security->generateCsrfToken(); | ||||||
|  | 
 | ||||||
|  |         // Load the forgot password form
 | ||||||
|  |         include '../app/helpers/feedback.php'; | ||||||
|  |         include '../app/templates/form-password-forgot.php'; | ||||||
|  |         exit(); | ||||||
|  | 
 | ||||||
|  |     } elseif ($action === 'reset' && isset($_GET['token'])) { | ||||||
|  |         // Handle password reset
 | ||||||
|  |         try { | ||||||
|  |             require_once '../app/classes/passwordReset.php'; | ||||||
|  |             $resetHandler = new PasswordReset($db, $config); | ||||||
|  |             $token = $_GET['token']; | ||||||
|  | 
 | ||||||
|  |             if ($_SERVER['REQUEST_METHOD'] === 'POST') { | ||||||
|  |                 // Validate CSRF token
 | ||||||
|  |                 $security = SecurityHelper::getInstance(); | ||||||
|  |                 if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) { | ||||||
|  |                     throw new Exception('Invalid security token. Please try again.'); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Apply rate limiting
 | ||||||
|  |                 if (!$rateLimiter->isIpWhitelisted($user_IP)) { | ||||||
|  |                     if ($rateLimiter->tooManyAttempts('password_reset', $user_IP)) { | ||||||
|  |                         throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']); | ||||||
|  |                     } | ||||||
|  |                     $rateLimiter->attempt('password_reset', $user_IP); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Validate password
 | ||||||
|  |                 require_once '../app/classes/validator.php'; | ||||||
|  |                 $validator = new Validator($_POST); | ||||||
|  |                 $rules = [ | ||||||
|  |                     'new_password' => [ | ||||||
|  |                         'required' => true, | ||||||
|  |                         'min' => 8 | ||||||
|  |                     ], | ||||||
|  |                     'confirm_password' => [ | ||||||
|  |                         'required' => true, | ||||||
|  |                         'matches' => 'new_password' | ||||||
|  |                     ] | ||||||
|  |                 ]; | ||||||
|  | 
 | ||||||
|  |                 if (!$validator->validate($rules)) { | ||||||
|  |                     throw new Exception($validator->getFirstError()); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Reset password
 | ||||||
|  |                 if ($resetHandler->resetPassword($token, $_POST['new_password'])) { | ||||||
|  |                     Feedback::flash('NOTICE', 'DEFAULT', 'Your password has been reset successfully. You can now log in.'); | ||||||
|  |                     header("Location: $app_root?page=login"); | ||||||
|  |                     exit(); | ||||||
|  |                 } | ||||||
|  |                 throw new Exception('Invalid or expired reset link. Please request a new one.'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Verify token is valid
 | ||||||
|  |             $validation = $resetHandler->validateToken($token); | ||||||
|  |             if (!$validation['valid']) { | ||||||
|  |                 throw new Exception('Invalid or expired reset link. Please request a new one.'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Show reset password form
 | ||||||
|  |             include '../app/helpers/feedback.php'; | ||||||
|  |             include '../app/templates/form-password-reset.php'; | ||||||
|  |             exit(); | ||||||
|  | 
 | ||||||
|  |         } catch (Exception $e) { | ||||||
|  |             Feedback::flash('ERROR', 'DEFAULT', $e->getMessage()); | ||||||
|  |             header("Location: $app_root?page=login&action=forgot"); | ||||||
|  |             exit(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ( $_SERVER['REQUEST_METHOD'] == 'POST' && $action !== 'verify' ) { | ||||||
|  |         try { | ||||||
|  |             // Validate form data
 | ||||||
|  |             $security = SecurityHelper::getInstance(); | ||||||
|  |             $formData = $security->sanitizeArray($_POST, ['username', 'password', 'remember_me', 'csrf_token']); | ||||||
|  | 
 | ||||||
|  |             $validationRules = [ | ||||||
|  |                 'username' => [ | ||||||
|  |                     'type' => 'string', | ||||||
|  |                     'required' => true, | ||||||
|  |                     'min' => 3, | ||||||
|  |                     'max' => 20 | ||||||
|  |                 ], | ||||||
|  |                 'password' => [ | ||||||
|  |                     'type' => 'string', | ||||||
|  |                     'required' => true | ||||||
|  |                 ] | ||||||
|  |             ]; | ||||||
|  | 
 | ||||||
|  |             $errors = $security->validateFormData($formData, $validationRules); | ||||||
|  |             if (!empty($errors)) { | ||||||
|  |                 throw new Exception("Invalid input: " . implode(", ", $errors)); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             $username = $formData['username']; | ||||||
|  |             $password = $formData['password']; | ||||||
|  | 
 | ||||||
|  |             // Skip all checks if IP is whitelisted
 | ||||||
|  |             if (!$rateLimiter->isIpWhitelisted($user_IP)) { | ||||||
|  |                 // Check if IP is blacklisted
 | ||||||
|  |                 if ($rateLimiter->isIpBlacklisted($user_IP)) { | ||||||
|  |                     throw new Exception(Feedback::get('LOGIN', 'IP_BLACKLISTED')['message']); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Check rate limiting before recording attempt
 | ||||||
|  |                 if ($rateLimiter->tooManyAttempts($username, $user_IP)) { | ||||||
|  |                     throw new Exception(Feedback::get('LOGIN', 'TOO_MANY_ATTEMPTS')['message']); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Attempt login
 | ||||||
|  |             $loginResult = $userObject->login($username, $password); | ||||||
|  | 
 | ||||||
|  |             if (is_array($loginResult)) { | ||||||
|  |                 switch ($loginResult['status']) { | ||||||
|  |                     case 'requires_2fa': | ||||||
|  |                         // Store pending 2FA info
 | ||||||
|  |                         Session::store2FAPending($loginResult['user_id'], $loginResult['username'], | ||||||
|  |                             isset($formData['remember_me'])); | ||||||
|  | 
 | ||||||
|  |                         // Redirect to 2FA verification
 | ||||||
|  |                         header('Location: ?page=login&action=verify'); | ||||||
|  |                         exit(); | ||||||
|  | 
 | ||||||
|  |                     case 'success': | ||||||
|  |                         // Complete login
 | ||||||
|  |                         handleSuccessfulLogin($loginResult['user_id'], $loginResult['username'], | ||||||
|  |                             isset($formData['remember_me']), $config, $app_root, $logObject, $user_IP); | ||||||
|  |                         exit(); | ||||||
|  | 
 | ||||||
|  |                     default: | ||||||
|  |                         throw new Exception($loginResult['message'] ?? 'Login failed'); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             throw new Exception(Feedback::get('LOGIN', 'LOGIN_FAILED')['message']); | ||||||
|         } catch (Exception $e) { |         } catch (Exception $e) { | ||||||
|             // Log the failed attempt
 |             // Log the failed attempt
 | ||||||
|             Messages::flash('ERROR', 'DEFAULT', $e->getMessage()); |             Feedback::flash('ERROR', 'DEFAULT', $e->getMessage()); | ||||||
|             if (isset($username)) { |             if (isset($username)) { | ||||||
|                 $user_id = $userObject->getUserId($username)[0]['id'] ?? 0; |                 $userId = $userObject->getUserId($username)[0]['id'] ?? 0; | ||||||
|                 $logObject->insertLog($user_id, "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", 'user');
 |                 $logObject->log('error', "Login: Failed login attempt for user \"$username\". IP: $user_IP. Reason: {$e->getMessage()}", ['user_id' => $userId, 'scope' => 'user']);
 | ||||||
|  |                 $rateLimiter->attempt($username, $user_IP); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } catch (Exception $e) { | } catch (Exception $e) { | ||||||
|     Messages::flash('ERROR', 'DEFAULT', 'There was an unexpected error. Please try again.'); |     Feedback::flash('ERROR', 'DEFAULT'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Show configured login message if any
 | // Show configured login message if any
 | ||||||
| if (!empty($config['login_message'])) { | if (!empty($config['login_message'])) { | ||||||
|     echo Messages::render('NOTICE', 'DEFAULT', $config['login_message'], false, false, false); |     echo Feedback::render('NOTICE', 'DEFAULT', $config['login_message'], false, false, false); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Get any new messages
 | // Get any new feedback messages
 | ||||||
| include '../app/includes/messages.php'; | include '../app/helpers/feedback.php'; | ||||||
| include '../app/includes/messages-show.php'; |  | ||||||
| 
 | 
 | ||||||
| // Load the template
 | // Load the template
 | ||||||
| include '../app/templates/form-login.php'; | include '../app/templates/form-login.php'; | ||||||
| 
 | 
 | ||||||
| ?>
 | /** | ||||||
|  |  * Handle successful login by setting up session and cookies | ||||||
|  |  */ | ||||||
|  | function handleSuccessfulLogin($userId, $username, $rememberMe, $config, $app_root, $logObject, $userIP) { | ||||||
|  |     // Create authenticated session
 | ||||||
|  |     Session::createAuthSession($userId, $username, $rememberMe, $config); | ||||||
|  | 
 | ||||||
|  |     // Log successful login
 | ||||||
|  |     $logObject->log('info', "Login: User \"$username\" logged in. IP: $userIP", ['user_id' => $userId, 'scope' => 'user']);
 | ||||||
|  | 
 | ||||||
|  |     // Set success message
 | ||||||
|  |     Feedback::flash('LOGIN', 'LOGIN_SUCCESS'); | ||||||
|  | 
 | ||||||
|  |     // After successful login, redirect to original page if provided in URL param or POST
 | ||||||
|  |     $redirect = $app_root; | ||||||
|  |     $candidate = $_POST['redirect'] ?? $_GET['redirect'] ?? ''; | ||||||
|  |     $trimmed = trim($candidate, '/?'); | ||||||
|  |     if ( | ||||||
|  |         (strpos($candidate, '/') === 0 || strpos($candidate, '?') === 0) | ||||||
|  |         && !in_array($trimmed, INVALID_REDIRECT_PAGES, true) | ||||||
|  |     ) { | ||||||
|  |         $redirect = $candidate; | ||||||
|  |     } | ||||||
|  |     header('Location: ' . htmlspecialchars($redirect)); | ||||||
|  |     exit(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,83 +0,0 @@ | ||||||
| <?php |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * Logs listings |  | ||||||
|  * |  | ||||||
|  * This page ("logs") retrieves and displays logs for a specified user within a time range. |  | ||||||
|  * It supports pagination and filtering, and generates a widget to display the logs. |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| // Get any new messages
 |  | ||||||
| include '../app/includes/messages.php'; |  | ||||||
| include '../app/includes/messages-show.php'; |  | ||||||
| 
 |  | ||||||
| // Check for rights; user or system
 |  | ||||||
| if (($userObject->hasRight($user_id, 'superuser') || |  | ||||||
|       $userObject->hasRight($user_id, 'view app logs'))) { |  | ||||||
|     $scope = 'system'; |  | ||||||
| } else { |  | ||||||
|     $scope = 'user'; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // specify time range
 |  | ||||||
| include '../app/helpers/time_range.php'; |  | ||||||
| 
 |  | ||||||
| // pagination variables
 |  | ||||||
| $items_per_page = 15; |  | ||||||
| $browse_page = $_REQUEST['p'] ?? 1; |  | ||||||
| $browse_page = (int)$browse_page; |  | ||||||
| $offset = ($browse_page -1) * $items_per_page; |  | ||||||
| 
 |  | ||||||
| // prepare the result
 |  | ||||||
| $search = $logObject->readLog($user_id, $scope, $offset, $items_per_page); |  | ||||||
| $search_all = $logObject->readLog($user_id, $scope); |  | ||||||
| 
 |  | ||||||
| if (!empty($search)) { |  | ||||||
|     // we get total items and number of pages
 |  | ||||||
|     $item_count = count($search_all); |  | ||||||
|     $page_count = ceil($item_count / $items_per_page); |  | ||||||
| 
 |  | ||||||
|     $logs = array(); |  | ||||||
|     $logs['records'] = array(); |  | ||||||
| 
 |  | ||||||
|     foreach ($search as $item) { |  | ||||||
| 
 |  | ||||||
|         // when we show only user's logs, omit user_id column
 |  | ||||||
|         if ($scope === 'user') { |  | ||||||
|             $log_record = array( |  | ||||||
|                 // assign title to the field in the array record
 |  | ||||||
|                 'time'		=> $item['time'], |  | ||||||
|                 'log message'	=> $item['message'] |  | ||||||
|             ); |  | ||||||
|         } else { |  | ||||||
|             $log_record = array( |  | ||||||
|                 // assign title to the field in the array record
 |  | ||||||
|                 'userID'	=> $item['user_id'], |  | ||||||
|                 'time'		=> $item['time'], |  | ||||||
|                 'log message'	=> $item['message'] |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         // populate the result array
 |  | ||||||
|         array_push($logs['records'], $log_record); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // prepare the widget
 |  | ||||||
| $widget['full'] = false; |  | ||||||
| $widget['collapsible'] = false; |  | ||||||
| $widget['name'] = 'Logs'; |  | ||||||
| $username = $userObject->getUserDetails($user_id)[0]['username']; |  | ||||||
| $widget['title'] = "Log events for user \"$username\"";
 |  | ||||||
| $widget['filter'] = true; |  | ||||||
| if (!empty($logs['records'])) { |  | ||||||
|     $widget['full'] = true; |  | ||||||
|     $widget['table_headers'] = array_keys($logs['records'][0]); |  | ||||||
|     $widget['table_records'] = $logs['records']; |  | ||||||
| } |  | ||||||
| $widget['pagination'] = true; |  | ||||||
| 
 |  | ||||||
| // display the widget
 |  | ||||||
| include '../app/templates/logs-list.php'; |  | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  | @ -8,18 +8,12 @@ | ||||||
|  * Supports pagination. |  * Supports pagination. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| // Get any new messages
 |  | ||||||
| include '../app/includes/messages.php'; |  | ||||||
| include '../app/includes/messages-show.php'; |  | ||||||
| 
 |  | ||||||
| require '../app/classes/participant.php'; |  | ||||||
| 
 |  | ||||||
| // connect to database
 | // connect to database
 | ||||||
| $response = connectDB($config, 'jilo', $platformDetails[0]['jilo_database'], $platform_id); | $response = connectJiloDB($config, $platformDetails[0]['jilo_database'], $platform_id); | ||||||
| 
 | 
 | ||||||
| // if DB connection has error, display it and stop here
 | // if DB connection has error, display it and stop here
 | ||||||
| if ($response['db'] === null) { | if ($response['db'] === null) { | ||||||
|     Messages::flash('ERROR', 'DEFAULT', $response['error']); |     Feedback::flash('ERROR', 'DEFAULT', $response['error']); | ||||||
| 
 | 
 | ||||||
| // otherwise if DB connection is OK, go on
 | // otherwise if DB connection is OK, go on
 | ||||||
| } else { | } else { | ||||||
|  | @ -52,13 +46,34 @@ if ($response['db'] === null) { | ||||||
|     // Participant listings
 |     // Participant listings
 | ||||||
|     //
 |     //
 | ||||||
| 
 | 
 | ||||||
|  |     require '../app/classes/participant.php'; | ||||||
|     $participantObject = new Participant($db); |     $participantObject = new Participant($db); | ||||||
| 
 | 
 | ||||||
|  |     // get current page for pagination
 | ||||||
|  |     $currentPage = $_REQUEST['page_num'] ?? 1; | ||||||
|  |     $currentPage = (int)$currentPage; | ||||||
|  | 
 | ||||||
|     // pagination variables
 |     // pagination variables
 | ||||||
|     $items_per_page = 15; |     $items_per_page = 20; | ||||||
|     $browse_page = $_REQUEST['p'] ?? 1; |     $offset = ($currentPage -1) * $items_per_page; | ||||||
|     $browse_page = (int)$browse_page; | 
 | ||||||
|     $offset = ($browse_page -1) * $items_per_page; |     // Build params for pagination
 | ||||||
|  |     $params = ''; | ||||||
|  |     if (!empty($_REQUEST['from_time'])) { | ||||||
|  |         $params .= '&from_time=' . urlencode($_REQUEST['from_time']); | ||||||
|  |     } | ||||||
|  |     if (!empty($_REQUEST['until_time'])) { | ||||||
|  |         $params .= '&until_time=' . urlencode($_REQUEST['until_time']); | ||||||
|  |     } | ||||||
|  |     if (!empty($_REQUEST['name'])) { | ||||||
|  |         $params .= '&name=' . urlencode($_REQUEST['name']); | ||||||
|  |     } | ||||||
|  |     if (!empty($_REQUEST['id'])) { | ||||||
|  |         $params .= '&id=' . urlencode($_REQUEST['id']); | ||||||
|  |     } | ||||||
|  |     if (isset($_REQUEST['event'])) { | ||||||
|  |         $params .= '&ip=' . urlencode($_REQUEST['ip']); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     // search and list specific participant ID
 |     // search and list specific participant ID
 | ||||||
|     if (isset($participantId)) { |     if (isset($participantId)) { | ||||||
|  | @ -82,7 +97,7 @@ if ($response['db'] === null) { | ||||||
|     if (!empty($search)) { |     if (!empty($search)) { | ||||||
|         // we get total items and number of pages
 |         // we get total items and number of pages
 | ||||||
|         $item_count = count($search_all); |         $item_count = count($search_all); | ||||||
|         $page_count = ceil($item_count / $items_per_page); |         $totalPages = ceil($item_count / $items_per_page); | ||||||
| 
 | 
 | ||||||
|         $participants = array(); |         $participants = array(); | ||||||
|         $participants['records'] = array(); |         $participants['records'] = array(); | ||||||
|  | @ -144,34 +159,20 @@ if ($response['db'] === null) { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // prepare the widget
 |     // filter message
 | ||||||
|     $widget['full'] = false; |     $filterMessage = array(); | ||||||
|     $widget['name'] = 'Participants'; |  | ||||||
|     $widget['collapsible'] = false; |  | ||||||
|     $widget['collapsed'] = false; |  | ||||||
|     $widget['filter'] = true; |  | ||||||
|     $widget['pagination'] = true; |  | ||||||
| 
 |  | ||||||
|     // widget title
 |  | ||||||
|     if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') { |     if (isset($_REQUEST['name']) && $_REQUEST['name'] != '') { | ||||||
|         $widget['title'] = 'Conferences with participant name (stats_id) matching "<strong>' . $_REQUEST['name'] . '"</strong>'; |         array_push($filterMessage, 'Conferences with participant name (stats_id) matching "<strong>' . $_REQUEST['name'] . '</strong>"'); | ||||||
|     } elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') { |     } elseif (isset($_REQUEST['id']) && $_REQUEST['id'] != '') { | ||||||
|         $widget['title'] = 'Conference with participant ID matching "<strong>' . $_REQUEST['id'] . '"</strong>'; |         array_push($filterMessage, 'Conferences with participant ID matching "<strong>' . $_REQUEST['id'] . '</strong>"'); | ||||||
|     } elseif (isset($participantIp)) { |     } elseif (isset($participantIp)) { | ||||||
|         $widget['title'] = 'Conference with participant IP matching "<strong>' . $participantIp . '"</strong>'; |         array_push($filterMessage, 'Conferences with participant IP matching "<strong>' . $participantIp . '</strong>"'); | ||||||
|     } else { |  | ||||||
|         $widget['title'] = 'All participants'; |  | ||||||
|     } |  | ||||||
|     // widget records
 |  | ||||||
|     if (!empty($participants['records'])) { |  | ||||||
|         $widget['full'] = true; |  | ||||||
|         $widget['table_headers'] = array_keys($participants['records'][0]); |  | ||||||
|         $widget['table_records'] = $participants['records']; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     // Get any new feedback messages
 | ||||||
|  |     include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|     // display the widget
 |     // display the widget
 | ||||||
|     include '../app/templates/widget.php'; |     include '../app/templates/participants.php'; | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -12,24 +12,47 @@ | ||||||
|  * - `edit`: Edit user profile details, rights, or avatar. |  * - `edit`: Edit user profile details, rights, or avatar. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| // Get any new messages
 |  | ||||||
| include '../app/includes/messages.php'; |  | ||||||
| include '../app/includes/messages-show.php'; |  | ||||||
| 
 |  | ||||||
| $action = $_REQUEST['action'] ?? ''; | $action = $_REQUEST['action'] ?? ''; | ||||||
|  | $item = $_REQUEST['item'] ?? ''; | ||||||
| 
 | 
 | ||||||
| // if a form is submitted, it's from the edit page
 | // if a form is submitted, it's from the edit page
 | ||||||
| if ($_SERVER['REQUEST_METHOD'] == 'POST') { | if ($_SERVER['REQUEST_METHOD'] == 'POST') { | ||||||
|  |     // Validate CSRF token
 | ||||||
|  |     require_once '../app/helpers/security.php'; | ||||||
|  |     $security = SecurityHelper::getInstance(); | ||||||
|  |     if (!$security->verifyCsrfToken($_POST['csrf_token'] ?? '')) { | ||||||
|  |         Feedback::flash('ERROR', 'DEFAULT', 'Invalid security token. Please try again.'); | ||||||
|  |         header("Location: $app_root?page=profile"); | ||||||
|  |         exit(); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     $item = $_REQUEST['item'] ?? ''; |     require_once '../app/classes/validator.php'; | ||||||
|  | 
 | ||||||
|  |     // Apply rate limiting for profile operations
 | ||||||
|  |     require_once '../app/includes/rate_limit_middleware.php'; | ||||||
|  |     checkRateLimit($db, 'profile', $userId); | ||||||
| 
 | 
 | ||||||
|     // avatar removal
 |     // avatar removal
 | ||||||
|     if ($item === 'avatar' && $action === 'remove') { |     if ($item === 'avatar' && $action === 'remove') { | ||||||
|         $result = $userObject->removeAvatar($user_id, $config['avatars_path'].$userDetails[0]['avatar']); |         $validator = new Validator(['user_id' => $userId]); | ||||||
|  |         $rules = [ | ||||||
|  |             'user_id' => [ | ||||||
|  |                 'required' => true, | ||||||
|  |                 'numeric' => true | ||||||
|  |             ] | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         if (!$validator->validate($rules)) { | ||||||
|  |             Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError()); | ||||||
|  |             header("Location: $app_root?page=profile"); | ||||||
|  |             exit(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         $result = $userObject->removeAvatar($userId, $config['avatars_path'].$userDetails[0]['avatar']); | ||||||
|         if ($result === true) { |         if ($result === true) { | ||||||
|             $_SESSION['notice'] .= "Avatar for user \"{$userDetails[0]['username']}\" is removed. "; |             Feedback::flash('NOTICE', 'DEFAULT', "Avatar for user \"{$userDetails[0]['username']}\" is removed."); | ||||||
|         } else { |         } else { | ||||||
|             $_SESSION['error'] .= "Removing the avatar failed. Error: $result "; |             Feedback::flash('ERROR', 'DEFAULT', "Removing the avatar failed. Error: $result"); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         header("Location: $app_root?page=profile"); |         header("Location: $app_root?page=profile"); | ||||||
|  | @ -37,41 +60,83 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // update the profile
 |     // update the profile
 | ||||||
|  |     $validator = new Validator($_POST); | ||||||
|  |     $rules = [ | ||||||
|  |         'name' => [ | ||||||
|  |             'max' => 100 | ||||||
|  |         ], | ||||||
|  |         'email' => [ | ||||||
|  |             'email' => true, | ||||||
|  |             'max' => 100 | ||||||
|  |         ], | ||||||
|  |         'timezone' => [ | ||||||
|  |             'max' => 50 | ||||||
|  |         ], | ||||||
|  |         'bio' => [ | ||||||
|  |             'max' => 1000 | ||||||
|  |         ] | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     if (!$validator->validate($rules)) { | ||||||
|  |         Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError()); | ||||||
|  |         header("Location: $app_root?page=profile"); | ||||||
|  |         exit(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     $updatedUser = [ |     $updatedUser = [ | ||||||
|             'name'		=> $_POST['name'] ?? '', |         'name' => htmlspecialchars($_POST['name'] ?? ''), | ||||||
|             'email'		=> $_POST['email'] ?? '', |         'email' => filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL), | ||||||
|             'timezone'		=> $_POST['timezone'] ?? '', |         'timezone' => htmlspecialchars($_POST['timezone'] ?? ''), | ||||||
|             'bio'		=> $_POST['bio'] ?? '', |         'bio' => htmlspecialchars($_POST['bio'] ?? ''), | ||||||
|         ]; |     ]; | ||||||
|     $result = $userObject->editUser($user_id, $updatedUser); |     $result = $userObject->editUser($userId, $updatedUser); | ||||||
|     if ($result === true) { |     if ($result === true) { | ||||||
|         $_SESSION['notice'] .= "User details for \"{$updatedUser['name']}\" are edited. "; |         Feedback::flash('NOTICE', 'DEFAULT', "User details for \"{$userDetails[0]['username']}\" are edited."); | ||||||
|     } else { |     } else { | ||||||
|         $_SESSION['error'] .= "Editing the user details failed. Error: $result "; |         Feedback::flash('ERROR', 'DEFAULT', "Editing the user details failed. Error: $result"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // update the rights
 |     // update the rights
 | ||||||
|     $newRights = $_POST['rights'] ?? array(); |     // Get current rights IDs
 | ||||||
|     // extract the new right_ids
 |  | ||||||
|     $userRightsIds = array_column($userRights, 'right_id'); |     $userRightsIds = array_column($userRights, 'right_id'); | ||||||
|  | 
 | ||||||
|  |     // If no rights are selected, remove all rights
 | ||||||
|  |     if (!isset($_POST['rights'])) { | ||||||
|  |         $_POST['rights'] = []; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     $validator = new Validator(['rights' => $_POST['rights']]); | ||||||
|  |     $rules = [ | ||||||
|  |         'rights' => [ | ||||||
|  |             'array' => true | ||||||
|  |         ] | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     if (!$validator->validate($rules)) { | ||||||
|  |         Feedback::flash('ERROR', 'DEFAULT', $validator->getFirstError()); | ||||||
|  |         header("Location: $app_root?page=profile"); | ||||||
|  |         exit(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     $newRights = $_POST['rights']; | ||||||
|     // what rights we need to add
 |     // what rights we need to add
 | ||||||
|     $rightsToAdd = array_diff($newRights, $userRightsIds); |     $rightsToAdd = array_diff($newRights, $userRightsIds); | ||||||
|     if (!empty($rightsToAdd)) { |     if (!empty($rightsToAdd)) { | ||||||
|         foreach ($rightsToAdd as $rightId) { |         foreach ($rightsToAdd as $rightId) { | ||||||
|             $userObject->addUserRight($user_id, $rightId); |             $userObject->addUserRight($userId, $rightId); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     // what rights we need to remove
 |     // what rights we need to remove
 | ||||||
|     $rightsToRemove = array_diff($userRightsIds, $newRights); |     $rightsToRemove = array_diff($userRightsIds, $newRights); | ||||||
|     if (!empty($rightsToRemove)) { |     if (!empty($rightsToRemove)) { | ||||||
|         foreach ($rightsToRemove as $rightId) { |         foreach ($rightsToRemove as $rightId) { | ||||||
|             $userObject->removeUserRight($user_id, $rightId); |             $userObject->removeUserRight($userId, $rightId); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // update the avatar
 |     // update the avatar
 | ||||||
|     if (!empty($_FILES['avatar_file']['tmp_name'])) { |     if (!empty($_FILES['avatar_file']['tmp_name'])) { | ||||||
|         $result = $userObject->changeAvatar($user_id, $_FILES['avatar_file'], $config['avatars_path']); |         $result = $userObject->changeAvatar($userId, $_FILES['avatar_file'], $config['avatars_path']); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     header("Location: $app_root?page=profile"); |     header("Location: $app_root?page=profile"); | ||||||
|  | @ -82,19 +147,30 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') { | ||||||
|     $avatar = !empty($userDetails[0]['avatar']) ? $config['avatars_path'] . $userDetails[0]['avatar'] : $config['default_avatar']; |     $avatar = !empty($userDetails[0]['avatar']) ? $config['avatars_path'] . $userDetails[0]['avatar'] : $config['default_avatar']; | ||||||
|     $default_avatar = empty($userDetails[0]['avatar']) ? true : false; |     $default_avatar = empty($userDetails[0]['avatar']) ? true : false; | ||||||
| 
 | 
 | ||||||
|     switch ($action) { |     // Generate CSRF token if not exists
 | ||||||
|  |     require_once '../app/helpers/security.php'; | ||||||
|  |     $security = SecurityHelper::getInstance(); | ||||||
|  |     $security->generateCsrfToken(); | ||||||
| 
 | 
 | ||||||
|  |     switch ($action) { | ||||||
|         case 'edit': |         case 'edit': | ||||||
|             $allRights = $userObject->getAllRights(); |             $allRights = $userObject->getAllRights(); | ||||||
|             $allTimezones = timezone_identifiers_list(); |             $allTimezones = timezone_identifiers_list(); | ||||||
|             // if timezone is already set, we pass a flag for JS to not autodetect browser timezone
 |             // if timezone is already set, we pass a flag for JS to not autodetect browser timezone
 | ||||||
|             $isTimezoneSet = !empty($userDetails[0]['timezone']); |             $isTimezoneSet = !empty($userDetails[0]['timezone']); | ||||||
|  | 
 | ||||||
|  |             // Get any new feedback messages
 | ||||||
|  |             include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  |             // Load the template
 | ||||||
|             include '../app/templates/profile-edit.php'; |             include '../app/templates/profile-edit.php'; | ||||||
|             break; |             break; | ||||||
| 
 | 
 | ||||||
|         default: |         default: | ||||||
|  |             // Get any new feedback messages
 | ||||||
|  |             include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  |             // Load the template
 | ||||||
|             include '../app/templates/profile.php'; |             include '../app/templates/profile.php'; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -1,54 +0,0 @@ | ||||||
| <?php |  | ||||||
| 
 |  | ||||||
| /** |  | ||||||
|  * User registration |  | ||||||
|  * |  | ||||||
|  * This page ("register") handles user registration if the feature is enabled in the configuration. |  | ||||||
|  * It accepts a POST request with a username and password, attempts to register the user, |  | ||||||
|  * and redirects to the login page on success or displays an error message on failure. |  | ||||||
|  */ |  | ||||||
| 
 |  | ||||||
| // registration is allowed, go on
 |  | ||||||
| if ($config['registration_enabled'] === true) { |  | ||||||
| 
 |  | ||||||
|     try { |  | ||||||
| 
 |  | ||||||
|         // connect to database
 |  | ||||||
|         $dbWeb = connectDB($config); |  | ||||||
| 
 |  | ||||||
|         if ( $_SERVER['REQUEST_METHOD'] == 'POST' ) { |  | ||||||
|             $username = $_POST['username']; |  | ||||||
|             $password = $_POST['password']; |  | ||||||
| 
 |  | ||||||
|             // registering
 |  | ||||||
|             $result = $userObject->register($username, $password); |  | ||||||
| 
 |  | ||||||
|             // redirect to login
 |  | ||||||
|             if ($result === true) { |  | ||||||
|                 Messages::flash('NOTICE', 'DEFAULT', "Registration successful.<br />You can log in now."); |  | ||||||
|                 header('Location: ' . htmlspecialchars($app_root)); |  | ||||||
|                 exit(); |  | ||||||
|             // registration fail, redirect to login
 |  | ||||||
|             } else { |  | ||||||
|                 Messages::flash('ERROR', 'DEFAULT', "Registration failed. $result"); |  | ||||||
|                 header('Location: ' . htmlspecialchars($app_root)); |  | ||||||
|                 exit(); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } catch (Exception $e) { |  | ||||||
|         Messages::flash('ERROR', 'DEFAULT', $e->getMessage()); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Get any new messages
 |  | ||||||
|     include '../app/includes/messages.php'; |  | ||||||
|     include '../app/includes/messages-show.php'; |  | ||||||
| 
 |  | ||||||
|     // Load the template
 |  | ||||||
|     include '../app/templates/form-register.php'; |  | ||||||
| 
 |  | ||||||
| // registration disabled
 |  | ||||||
| } else { |  | ||||||
|     echo Messages::render('NOTICE', 'DEFAULT', 'Registration is disabled', false); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  | @ -1,15 +1,10 @@ | ||||||
| <?php | <?php | ||||||
| 
 | 
 | ||||||
| // Check if user has any of the required rights
 | // Check if user has any of the required rights
 | ||||||
| if (!($userObject->hasRight($user_id, 'superuser') || | if (!($userObject->hasRight($userId, 'superuser') || | ||||||
|       $userObject->hasRight($user_id, 'edit whitelist') || |       $userObject->hasRight($userId, 'edit whitelist') || | ||||||
|       $userObject->hasRight($user_id, 'edit blacklist') || |       $userObject->hasRight($userId, 'edit blacklist') || | ||||||
|       $userObject->hasRight($user_id, 'edit ratelimiting'))) { |       $userObject->hasRight($userId, 'edit ratelimiting'))) { | ||||||
|     include '../app/templates/error-unauthorized.php'; |  | ||||||
|     exit; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| if (!isset($currentUser)) { |  | ||||||
|     include '../app/templates/error-unauthorized.php'; |     include '../app/templates/error-unauthorized.php'; | ||||||
|     exit; |     exit; | ||||||
| } | } | ||||||
|  | @ -19,95 +14,161 @@ $section = isset($_POST['section']) ? $_POST['section'] : (isset($_GET['section' | ||||||
| 
 | 
 | ||||||
| // Initialize RateLimiter
 | // Initialize RateLimiter
 | ||||||
| require_once '../app/classes/ratelimiter.php'; | require_once '../app/classes/ratelimiter.php'; | ||||||
| $rateLimiter = new RateLimiter($dbWeb); | $rateLimiter = new RateLimiter($db); | ||||||
| 
 | 
 | ||||||
| // Handle form submissions
 | // Handle form submissions
 | ||||||
| if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { | if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) { | ||||||
|  |     require_once '../app/classes/validator.php'; | ||||||
|  | 
 | ||||||
|  |     // Apply rate limiting for security operations
 | ||||||
|  |     require_once '../app/includes/rate_limit_middleware.php'; | ||||||
|  |     checkRateLimit($db, 'security', $userId); | ||||||
|  | 
 | ||||||
|     $action = $_POST['action']; |     $action = $_POST['action']; | ||||||
|  |     $validator = new Validator($_POST); | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|         switch ($action) { |         switch ($action) { | ||||||
|             case 'add_whitelist': |             case 'add_whitelist': | ||||||
|                 if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit whitelist')) { |                 if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit whitelist')) { | ||||||
|                     throw new Exception(Messages::get('SECURITY', 'PERMISSION_DENIED')['message']); |                     Feedback::flash('SECURITY', 'PERMISSION_DENIED'); | ||||||
|  |                     break; | ||||||
|                 } |                 } | ||||||
|                 if (empty($_POST['ip_address'])) { | 
 | ||||||
|                     throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']); |                 $rules = [ | ||||||
|  |                     'ip_address' => [ | ||||||
|  |                         'required' => true, | ||||||
|  |                         'max' => 45, // Max length for IPv6
 | ||||||
|  |                         'ip' => true | ||||||
|  |                     ], | ||||||
|  |                     'description' => [ | ||||||
|  |                         'required' => true, | ||||||
|  |                         'max' => 255 | ||||||
|  |                     ] | ||||||
|  |                 ]; | ||||||
|  | 
 | ||||||
|  |                 if ($validator->validate($rules)) { | ||||||
|  |                     $is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on'; | ||||||
|  |                     if (!$rateLimiter->addToWhitelist($_POST['ip_address'], $is_network, $_POST['description'] ?? '', $currentUser, $userId)) { | ||||||
|  |                         Feedback::flash('SECURITY', 'WHITELIST_ADD_FAILED'); | ||||||
|  |                     } else { | ||||||
|  |                         Feedback::flash('SECURITY', 'WHITELIST_ADD_SUCCESS'); | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     Feedback::flash('SECURITY', 'WHITELIST_ADD_ERROR_IP', $validator->getFirstError()); | ||||||
|                 } |                 } | ||||||
|                 $is_network = isset($_POST['is_network']) ? 1 : 0; |  | ||||||
|                 if (!$rateLimiter->addToWhitelist($_POST['ip_address'], $is_network, $_POST['description'] ?? '', $currentUser, $user_id)) { |  | ||||||
|                     throw new Exception(Messages::get('SECURITY', 'WHITELIST_ADD_ERROR')['message']); |  | ||||||
|                 } |  | ||||||
|                 Messages::flash('SECURITY', 'WHITELIST_ADD_SUCCESS'); |  | ||||||
|                 break; |                 break; | ||||||
| 
 | 
 | ||||||
|             case 'remove_whitelist': |             case 'remove_whitelist': | ||||||
|                 if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit whitelist')) { |                 if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit whitelist')) { | ||||||
|                     throw new Exception(Messages::get('SECURITY', 'PERMISSION_DENIED')['message']); |                     Feedback::flash('SECURITY', 'PERMISSION_DENIED'); | ||||||
|  |                     break; | ||||||
|                 } |                 } | ||||||
|                 if (empty($_POST['ip_address'])) { | 
 | ||||||
|                     throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']); |                 $rules = [ | ||||||
|  |                     'ip_address' => [ | ||||||
|  |                         'required' => true, | ||||||
|  |                         'max' => 45, | ||||||
|  |                         'ip' => true | ||||||
|  |                     ] | ||||||
|  |                 ]; | ||||||
|  | 
 | ||||||
|  |                 if ($validator->validate($rules)) { | ||||||
|  |                     if (!$rateLimiter->removeFromWhitelist($_POST['ip_address'], $currentUser, $userId)) { | ||||||
|  |                         Feedback::flash('SECURITY', 'WHITELIST_REMOVE_FAILED'); | ||||||
|  |                     } else { | ||||||
|  |                         Feedback::flash('SECURITY', 'WHITELIST_REMOVE_SUCCESS'); | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     Feedback::flash('SECURITY', 'WHITELIST_REMOVE_FAILED', $validator->getFirstError()); | ||||||
|                 } |                 } | ||||||
|                 if (!$rateLimiter->removeFromWhitelist($_POST['ip_address'], $currentUser, $user_id)) { |  | ||||||
|                     throw new Exception(Messages::get('SECURITY', 'WHITELIST_REMOVE_ERROR')['message']); |  | ||||||
|                 } |  | ||||||
|                 Messages::flash('SECURITY', 'WHITELIST_REMOVE_SUCCESS'); |  | ||||||
|                 break; |                 break; | ||||||
| 
 | 
 | ||||||
|             case 'add_blacklist': |             case 'add_blacklist': | ||||||
|                 if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit blacklist')) { |                 if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit blacklist')) { | ||||||
|                     throw new Exception(Messages::get('SECURITY', 'PERMISSION_DENIED')['message']); |                     Feedback::flash('SECURITY', 'PERMISSION_DENIED'); | ||||||
|  |                     break; | ||||||
|                 } |                 } | ||||||
|                 if (empty($_POST['ip_address'])) { | 
 | ||||||
|                     throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']); |                 $rules = [ | ||||||
|  |                     'ip_address' => [ | ||||||
|  |                         'required' => true, | ||||||
|  |                         'max' => 45, | ||||||
|  |                         'ip' => true | ||||||
|  |                     ], | ||||||
|  |                     'reason' => [ | ||||||
|  |                         'required' => true, | ||||||
|  |                         'max' => 255 | ||||||
|  |                     ], | ||||||
|  |                     'expiry_hours' => [ | ||||||
|  |                         'numeric' => true, | ||||||
|  |                         'min' => 0, | ||||||
|  |                         'max' => 8760 // 1 year in hours
 | ||||||
|  |                     ] | ||||||
|  |                 ]; | ||||||
|  | 
 | ||||||
|  |                 if ($validator->validate($rules)) { | ||||||
|  |                     $is_network = isset($_POST['is_network']) && $_POST['is_network'] === 'on'; | ||||||
|  |                     $expiry_hours = !empty($_POST['expiry_hours']) ? (int)$_POST['expiry_hours'] : null; | ||||||
|  | 
 | ||||||
|  |                     if (!$rateLimiter->addToBlacklist($_POST['ip_address'], $is_network, $_POST['reason'], $currentUser, $userId, $expiry_hours)) { | ||||||
|  |                         Feedback::flash('SECURITY', 'BLACKLIST_ADD_FAILED'); | ||||||
|  |                     } else { | ||||||
|  |                         Feedback::flash('SECURITY', 'BLACKLIST_ADD_SUCCESS'); | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     Feedback::flash('SECURITY', 'BLACKLIST_ADD_ERROR_IP', $validator->getFirstError()); | ||||||
|                 } |                 } | ||||||
|                 $is_network = isset($_POST['is_network']) ? 1 : 0; |  | ||||||
|                 $expiry_hours = !empty($_POST['expiry_hours']) ? intval($_POST['expiry_hours']) : null; |  | ||||||
|                 if (!$rateLimiter->addToBlacklist($_POST['ip_address'], $is_network, $_POST['reason'] ?? '', $currentUser, $user_id, $expiry_hours)) { |  | ||||||
|                     throw new Exception(Messages::get('SECURITY', 'BLACKLIST_ADD_ERROR')['message']); |  | ||||||
|                 } |  | ||||||
|                 Messages::flash('SECURITY', 'BLACKLIST_ADD_SUCCESS'); |  | ||||||
|                 break; |                 break; | ||||||
| 
 | 
 | ||||||
|             case 'remove_blacklist': |             case 'remove_blacklist': | ||||||
|                 if (!$userObject->hasRight($user_id, 'superuser') && !$userObject->hasRight($user_id, 'edit blacklist')) { |                 if (!$userObject->hasRight($userId, 'superuser') && !$userObject->hasRight($userId, 'edit blacklist')) { | ||||||
|                     throw new Exception(Messages::get('SECURITY', 'PERMISSION_DENIED')['message']); |                     Feedback::flash('SECURITY', 'PERMISSION_DENIED'); | ||||||
|  |                     break; | ||||||
|                 } |                 } | ||||||
|                 if (empty($_POST['ip_address'])) { | 
 | ||||||
|                     throw new Exception(Messages::get('SECURITY', 'IP_REQUIRED')['message']); |                 $rules = [ | ||||||
|  |                     'ip_address' => [ | ||||||
|  |                         'required' => true, | ||||||
|  |                         'max' => 45, | ||||||
|  |                         'ip' => true | ||||||
|  |                     ] | ||||||
|  |                 ]; | ||||||
|  | 
 | ||||||
|  |                 if ($validator->validate($rules)) { | ||||||
|  |                     if (!$rateLimiter->removeFromBlacklist($_POST['ip_address'], $currentUser, $userId)) { | ||||||
|  |                         Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_FAILED'); | ||||||
|  |                     } else { | ||||||
|  |                         Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_SUCCESS'); | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     Feedback::flash('SECURITY', 'BLACKLIST_REMOVE_FAILED', $validator->getFirstError()); | ||||||
|                 } |                 } | ||||||
|                 if (!$rateLimiter->removeFromBlacklist($_POST['ip_address'], $currentUser, $user_id)) { |  | ||||||
|                     throw new Exception(Messages::get('SECURITY', 'BLACKLIST_REMOVE_ERROR')['message']); |  | ||||||
|                 } |  | ||||||
|                 Messages::flash('SECURITY', 'BLACKLIST_REMOVE_SUCCESS'); |  | ||||||
|                 break; |                 break; | ||||||
|  | 
 | ||||||
|  |             default: | ||||||
|  |                 Feedback::flash('ERROR', 'INVALID_ACTION'); | ||||||
|         } |         } | ||||||
|     } catch (Exception $e) { |     } catch (Exception $e) { | ||||||
|         $messages[] = ['category' => 'SECURITY', 'key' => 'CUSTOM_ERROR', 'custom_message' => $e->getMessage()]; |         Feedback::flash('ERROR', $e->getMessage()); | ||||||
|         Messages::flash('SECURITY', 'CUSTOM_ERROR', 'custom_message'); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (empty($messages)) { |     // Redirect back to the appropriate section
 | ||||||
|         // Only redirect if there were no errors
 |     header("Location: $app_root?page=security§ion=" . urlencode($section)); | ||||||
|         header("Location: {$app_root}?page=security§ion={$section}"); |     exit; | ||||||
|         exit; |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Always show rate limit info message for rate limiting section
 | // Always show rate limit info message for rate limiting section
 | ||||||
| if ($section === 'ratelimit') { | if ($section === 'ratelimit') { | ||||||
|     $messages[] = ['category' => 'SECURITY', 'key' => 'RATE_LIMIT_INFO']; |     $system_messages[] = ['category' => 'SECURITY', 'key' => 'RATE_LIMIT_INFO']; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Get current lists
 | // Get current lists
 | ||||||
| $whitelisted = $rateLimiter->getWhitelistedIps(); | $whitelisted = $rateLimiter->getWhitelistedIps(); | ||||||
| $blacklisted = $rateLimiter->getBlacklistedIps(); | $blacklisted = $rateLimiter->getBlacklistedIps(); | ||||||
| 
 | 
 | ||||||
| // Get any new messages
 | // Get any new feedback messages
 | ||||||
| include '../app/includes/messages.php'; | include '../app/helpers/feedback.php'; | ||||||
| include '../app/includes/messages-show.php'; |  | ||||||
| 
 | 
 | ||||||
| // Load the template
 | // Load the template
 | ||||||
| include '../app/templates/security.php'; | include '../app/templates/security.php'; | ||||||
| 
 |  | ||||||
| ?>
 |  | ||||||
|  |  | ||||||
|  | @ -0,0 +1,179 @@ | ||||||
|  | <?php | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Jilo settings management. | ||||||
|  |  * | ||||||
|  |  * This page ("settings") handles Jilo settings by | ||||||
|  |  * adding, editing, and deleting platforms, hosts, agents. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | // Check if this is an AJAX request
 | ||||||
|  | $isAjax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&  | ||||||
|  |           strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'; | ||||||
|  | 
 | ||||||
|  | // Get any new feedback messages
 | ||||||
|  | include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  | $action = $_REQUEST['action'] ?? ''; | ||||||
|  | $agent = $_REQUEST['agent'] ?? ''; | ||||||
|  | $host = $_REQUEST['host'] ?? ''; | ||||||
|  | 
 | ||||||
|  | require '../app/classes/host.php'; | ||||||
|  | require '../app/classes/agent.php'; | ||||||
|  | 
 | ||||||
|  | $hostObject = new Host($db); | ||||||
|  | $agentObject = new Agent($db); | ||||||
|  | 
 | ||||||
|  | if ($_SERVER['REQUEST_METHOD'] == 'POST') { | ||||||
|  |     /** | ||||||
|  |      * Handles form submissions from editing | ||||||
|  |      */ | ||||||
|  | 
 | ||||||
|  |     // Apply rate limiting for profile operations
 | ||||||
|  |     require_once '../app/includes/rate_limit_middleware.php'; | ||||||
|  |     checkRateLimit($db, 'profile', $userId); | ||||||
|  | 
 | ||||||
|  |     // Get hash from URL if present
 | ||||||
|  |     $hash = parse_url($_SERVER['REQUEST_URI'], PHP_URL_FRAGMENT) ?? ''; | ||||||
|  |     $redirectUrl = htmlspecialchars($app_root) . '?page=settings'; | ||||||
|  |     if ($hash) { | ||||||
|  |         $redirectUrl .= '#' . $hash; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // host operations
 | ||||||
|  |     if (isset($_POST['item']) && $_POST['item'] === 'host') { | ||||||
|  |         if (isset($_POST['delete']) && $_POST['delete'] === 'true') { // This is a host delete
 | ||||||
|  |             $host_id = $_POST['host']; | ||||||
|  |             $result = $hostObject->deleteHost($host_id); | ||||||
|  |             if ($result === true) { | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', "Host deleted successfully.", true); | ||||||
|  |             } else { | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', "Deleting the host failed. Error: $result", true); | ||||||
|  |             } | ||||||
|  |         } else if (!isset($_POST['host'])) { // This is a new host
 | ||||||
|  |             $newHost = [ | ||||||
|  |                 'address'       => $_POST['address'], | ||||||
|  |                 'platform_id'   => $_POST['platform'], | ||||||
|  |                 'name'          => empty($_POST['name']) ? $_POST['address'] : $_POST['name'], | ||||||
|  |             ]; | ||||||
|  |             $result = $hostObject->addHost($newHost); | ||||||
|  |             if ($result === true) { | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', "New Jilo host added.", true); | ||||||
|  |             } else { | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', "Adding the host failed. Error: $result", true); | ||||||
|  |             } | ||||||
|  |         } else { // This is an edit of existing host
 | ||||||
|  |             $host_id = $_POST['host']; | ||||||
|  |             $platform_id = $_POST['platform']; | ||||||
|  |             $updatedHost = [ | ||||||
|  |                 'id'      => $host_id, | ||||||
|  |                 'address' => $_POST['address'], | ||||||
|  |                 'name'    => empty($_POST['name']) ? $_POST['address'] : $_POST['name'], | ||||||
|  |             ]; | ||||||
|  |             $result = $hostObject->editHost($platform_id, $updatedHost); | ||||||
|  |             if ($result === true) { | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', "Host edited.", true); | ||||||
|  |             } else { | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', "Editing the host failed. Error: $result", true); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (!$isAjax) { | ||||||
|  |             header('Location: ' . $redirectUrl); | ||||||
|  |             exit; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     // agent operations
 | ||||||
|  |     } elseif (isset($_POST['item']) && $_POST['item'] === 'agent') { | ||||||
|  |         if (isset($_POST['delete']) && $_POST['delete'] === 'true') { // This is an agent delete
 | ||||||
|  |             $agent_id = $_POST['agent']; | ||||||
|  |             $result = $agentObject->deleteAgent($agent_id); | ||||||
|  |             if ($result === true) { | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', "Agent deleted successfully.", true); | ||||||
|  |             } else { | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', "Deleting the agent failed. Error: $result", true); | ||||||
|  |             } | ||||||
|  |         } else if (isset($_POST['new']) && $_POST['new'] === 'true') { // This is a new agent
 | ||||||
|  |             $newAgent = [ | ||||||
|  |                 'type_id'       => $_POST['type'], | ||||||
|  |                 'url'           => $_POST['url'], | ||||||
|  |                 'secret_key'    => empty($_POST['secret_key']) ? null : $_POST['secret_key'], | ||||||
|  |                 'check_period'  => empty($_POST['check_period']) ? 0 : $_POST['check_period'], | ||||||
|  |             ]; | ||||||
|  |             $result = $agentObject->addAgent($_POST['host'], $newAgent); | ||||||
|  |             if ($result === true) { | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', "New Jilo agent added.", true); | ||||||
|  |             } else { | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', "Adding the agent failed. Error: $result", true); | ||||||
|  |             } | ||||||
|  |         } else { // This is an edit of existing agent
 | ||||||
|  |             $agent_id = $_POST['agent']; | ||||||
|  |             $updatedAgent = [ | ||||||
|  |                 'agent_type_id' => $_POST['agent_type_id'], | ||||||
|  |                 'url'          => $_POST['url'], | ||||||
|  |                 'secret_key'   => empty($_POST['secret_key']) ? null : $_POST['secret_key'], | ||||||
|  |                 'check_period' => empty($_POST['check_period']) ? 0 : $_POST['check_period'], | ||||||
|  |             ]; | ||||||
|  |             $result = $agentObject->editAgent($agent_id, $updatedAgent); | ||||||
|  |             if ($result === true) { | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', "Agent edited.", true); | ||||||
|  |             } else { | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', "Editing the agent failed. Error: $result", true); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (!$isAjax) { | ||||||
|  |             header('Location: ' . $redirectUrl); | ||||||
|  |             exit; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |     // platform operations
 | ||||||
|  |     } elseif (isset($_POST['item']) && $_POST['item'] === 'platform') { | ||||||
|  |         if (isset($_POST['delete']) && $_POST['delete'] === 'true') { // This is a platform delete
 | ||||||
|  |             $platform_id = $_POST['platform']; | ||||||
|  |             $result = $platformObject->deletePlatform($platform_id); | ||||||
|  |             if ($result === true) { | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', "Platform deleted successfully.", true); | ||||||
|  |             } else { | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', "Deleting the platform failed. Error: $result", true); | ||||||
|  |             } | ||||||
|  |         } else if (!isset($_POST['platform'])) { // This is a new platform
 | ||||||
|  |             $newPlatform = [ | ||||||
|  |                 'name'          => $_POST['name'], | ||||||
|  |                 'jitsi_url'     => $_POST['jitsi_url'], | ||||||
|  |                 'jilo_database' => $_POST['jilo_database'], | ||||||
|  |             ]; | ||||||
|  |             $result = $platformObject->addPlatform($newPlatform); | ||||||
|  |             if ($result === true) { | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', "New Jitsi platform added.", true); | ||||||
|  |             } else { | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', "Adding the platform failed. Error: $result", true); | ||||||
|  |             } | ||||||
|  |         } else { // This is an edit of existing platform
 | ||||||
|  |             $platform_id = $_POST['platform']; | ||||||
|  |             $updatedPlatform = [ | ||||||
|  |                 'name'          => $_POST['name'], | ||||||
|  |                 'jitsi_url'     => $_POST['jitsi_url'], | ||||||
|  |                 'jilo_database' => $_POST['jilo_database'], | ||||||
|  |             ]; | ||||||
|  |             $result = $platformObject->editPlatform($platform_id, $updatedPlatform); | ||||||
|  |             if ($result === true) { | ||||||
|  |                 Feedback::flash('NOTICE', 'DEFAULT', "Platform edited.", true); | ||||||
|  |             } else { | ||||||
|  |                 Feedback::flash('ERROR', 'DEFAULT', "Editing the platform failed. Error: $result", true); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         header('Location: ' . $redirectUrl); | ||||||
|  |         exit; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } else { | ||||||
|  |     /** | ||||||
|  |      * Handles GET requests to display templates. | ||||||
|  |      */ | ||||||
|  | 
 | ||||||
|  |     if ($userObject->hasRight($userId, 'view settings') || $userObject->hasRight($userId, 'superuser')) { | ||||||
|  |         $jilo_agent_types = $agentObject->getAgentTypes(); | ||||||
|  |         include '../app/templates/settings.php'; | ||||||
|  |     } else { | ||||||
|  |         include '../app/templates/error-unauthorized.php'; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -8,12 +8,13 @@ | ||||||
|  * It generates output for each platform and agent. |  * It generates output for each platform and agent. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| // Get any new messages
 | // Get any new feedback messages
 | ||||||
| include '../app/includes/messages.php'; | include '../app/helpers/feedback.php'; | ||||||
| include '../app/includes/messages-show.php'; |  | ||||||
| 
 | 
 | ||||||
| require '../app/classes/agent.php'; | require '../app/classes/agent.php'; | ||||||
| $agentObject = new Agent($dbWeb); | require '../app/classes/host.php'; | ||||||
|  | $agentObject = new Agent($db); | ||||||
|  | $hostObject = new Host($db); | ||||||
| 
 | 
 | ||||||
| include '../app/templates/status-server.php'; | include '../app/templates/status-server.php'; | ||||||
| 
 | 
 | ||||||
|  | @ -21,52 +22,60 @@ include '../app/templates/status-server.php'; | ||||||
| foreach ($platformsAll as $platform) { | foreach ($platformsAll as $platform) { | ||||||
| 
 | 
 | ||||||
|     // check if we can connect to the jilo database
 |     // check if we can connect to the jilo database
 | ||||||
|     $response = connectDB($config, 'jilo', $platform['jilo_database'], $platform['id']); |     $response = connectJiloDB($config, $platform['jilo_database'], $platform['id']); | ||||||
|     if ($response['error'] !== null) { |     if ($response['error'] !== null) { | ||||||
|         $jilo_database_status = '<span class="text-danger">' . htmlspecialchars($response['error']) . '</span>'; |         $jilo_database_status = $response['error']; | ||||||
|     } else { |     } else { | ||||||
|         $jilo_database_status = '<span class="text-success">OK</span>'; |         $jilo_database_status = 'Connected'; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     include '../app/templates/status-platform.php'; |     include '../app/templates/status-platform.php'; | ||||||
| 
 | 
 | ||||||
|     // fetch agent details for the current platform
 |     // fetch hosts for the current platform
 | ||||||
|     $agentDetails = $agentObject->getAgentDetails($platform['id']); |     $hostDetails = $hostObject->getHostDetails($platform['id']); | ||||||
|     foreach ($agentDetails as $agent) { |     foreach ($hostDetails as $host) { | ||||||
|         $agent_url = parse_url($agent['url']); |         // fetch agent details for the current host
 | ||||||
|         $agent_protocol = isset($agent_url['scheme']) ? $agent_url['scheme']: ''; |         $agentDetails = $agentObject->getAgentDetails($host['id']); | ||||||
|         $agent_host = isset($agent_url['host']) ? $agent_url['host']: ''; |         foreach ($agentDetails as $agent) { | ||||||
|         $agent_port = isset($agent_url['port']) ? $agent_url['port']: ''; |             // we try to parse the URL to scheme:/host:port
 | ||||||
|  |             $agent_url = parse_url($agent['url']); | ||||||
|  |             $agent_protocol = isset($agent_url['scheme']) ? $agent_url['scheme']: ''; | ||||||
|  |             // on failure we keep the full value for displaying purpose
 | ||||||
|  |             $agent_host = isset($agent_url['host']) ? $agent_url['host']: $agent['url']; | ||||||
|  |             $agent_port = isset($agent_url['port']) ? $agent_url['port']: ''; | ||||||
| 
 | 
 | ||||||
|         // we get agent data to check availability
 |             // we get agent data to check availability
 | ||||||
|         $agent_response = $agentObject->fetchAgent($agent['id'], true); |             $agent_response = $agentObject->fetchAgent($agent['id'], true); | ||||||
|         $agent_data = json_decode($agent_response); |             $agent_data = json_decode($agent_response); | ||||||
| 
 | 
 | ||||||
|         // determine agent availability based on response data
 |             // determine agent availability based on response data
 | ||||||
|         if (json_last_error() === JSON_ERROR_NONE) { |             if (json_last_error() === JSON_ERROR_NONE) { | ||||||
|             $agent_availability = '<span class="text-warning">unknown</span>'; |                 $agent_availability = 'unknown'; | ||||||
|             foreach ($agent_data as $key => $value) { |                 foreach ($agent_data as $key => $value) { | ||||||
|                 if ($key === 'error') { |                     if ($key === 'error') { | ||||||
|                     $agent_availability = '<span class="text-danger">' . htmlspecialchars($value) . '</span>'; |                         $agent_availability = $value; | ||||||
|                     break; |  | ||||||
|                 } |  | ||||||
|                 if (preg_match('/_state$/', $key)) { |  | ||||||
|                     if ($value === 'error') { |  | ||||||
|                         $agent_availability = '<span class="text-danger">not running</span>'; |  | ||||||
|                         break; |                         break; | ||||||
|                     } |                     } | ||||||
|                     if ($value === 'running') { |                     if (preg_match('/_state$/', $key)) { | ||||||
|                         $agent_availability = '<span class="text-success">running</span>'; |                         if ($value === 'error') { | ||||||
|                         break; |                             $agent_availability = 'not running'; | ||||||
|  |                             break; | ||||||
|  |                         } | ||||||
|  |                         if ($value === 'running') { | ||||||
|  |                             $agent_availability = 'running'; | ||||||
|  |                             break; | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |             } else { | ||||||
|  |                 $agent_availability = 'json error'; | ||||||
|             } |             } | ||||||
|         } else { | 
 | ||||||
|             $agent_availability = 'json error'; |             include '../app/templates/status-agent.php'; | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         include '../app/templates/status-agent.php'; |  | ||||||
|     } |     } | ||||||
|  |     echo "\n\t\t\t\t\t\t\t</div>\n"; | ||||||
| } | } | ||||||
| 
 | echo "\n\t\t\t\t\t\t</div>"; | ||||||
| ?>
 | echo "\n\t\t\t\t\t</div>"; | ||||||
|  | echo "\n\t\t\t\t</div>"; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * Theme Asset handler | ||||||
|  |  * | ||||||
|  |  * Serves theme assets through the main application router. | ||||||
|  |  * This provides a secure way to serve theme files that are outside the web root. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | // Include the theme asset handler
 | ||||||
|  | require_once __DIR__ . '/../helpers/theme-asset.php'; | ||||||
|  | @ -0,0 +1,71 @@ | ||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * Theme Management Controller | ||||||
|  |  * | ||||||
|  |  * Handles theme switching and management functionality. | ||||||
|  |  * Allows users to view available themes and change the active theme. | ||||||
|  |  * | ||||||
|  |  * Actions: | ||||||
|  |  * - switch_to: Changes the active theme for the current user | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | // Initialize security
 | ||||||
|  | require_once '../app/helpers/security.php'; | ||||||
|  | $security = SecurityHelper::getInstance(); | ||||||
|  | 
 | ||||||
|  | // Only allow access to logged-in users
 | ||||||
|  | if (!Session::isValidSession()) { | ||||||
|  |     header('Location: ' . $app_root . '?page=login'); | ||||||
|  |     exit; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Get any old feedback messages
 | ||||||
|  | include '../app/helpers/feedback.php'; | ||||||
|  | 
 | ||||||
|  | // Handle theme switching
 | ||||||
|  | if (isset($_GET['switch_to'])) { | ||||||
|  |     $themeName = $_GET['switch_to']; | ||||||
|  | 
 | ||||||
|  |     // Validate CSRF token for state-changing operations
 | ||||||
|  |     if (!$security->verifyCsrfToken($_GET['csrf_token'] ?? '')) { | ||||||
|  |         Feedback::flash('SECURITY', 'CSRF_INVALID'); | ||||||
|  |         header("Location: $app_root?page=theme"); | ||||||
|  |         exit(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (\App\Helpers\Theme::setCurrentTheme($themeName)) { | ||||||
|  |         // Set success message
 | ||||||
|  |         Feedback::flash('THEME', 'THEME_CHANGED'); | ||||||
|  |     } else { | ||||||
|  |         // Set error message
 | ||||||
|  |         Feedback::flash('THEME', 'THEME_CHANGE_FAILED'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Redirect back to prevent form resubmission
 | ||||||
|  |     $redirect = $app_root . '?page=theme'; | ||||||
|  |     header("Location: $redirect"); | ||||||
|  |     exit; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Get available themes and current theme for the view
 | ||||||
|  | $themes = \App\Helpers\Theme::getAvailableThemes(); | ||||||
|  | $currentTheme = \App\Helpers\Theme::getCurrentThemeName(); | ||||||
|  | 
 | ||||||
|  | // Prepare theme data with screenshot URLs for the view
 | ||||||
|  | $themeData = []; | ||||||
|  | foreach ($themes as $id => $name) { | ||||||
|  |     $themeData[$id] = [ | ||||||
|  |         'name' => $name, | ||||||
|  |         'screenshotUrl' => \App\Helpers\Theme::getAssetUrl($id, 'screenshot.png'), | ||||||
|  |         'isActive' => $id === $currentTheme | ||||||
|  |     ]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Make theme data available to the view
 | ||||||
|  | $themes = $themeData; | ||||||
|  | 
 | ||||||
|  | // Generate CSRF token for the form
 | ||||||
|  | $csrf_token = $security->generateCsrfToken(); | ||||||
|  | 
 | ||||||
|  | // Load the template
 | ||||||
|  | include '../app/templates/theme.php'; | ||||||
|  | @ -0,0 +1,139 @@ | ||||||
|  | <?php | ||||||
|  | /** @var bool $maintenance_enabled */ | ||||||
|  | /** @var string $maintenance_message */ | ||||||
|  | /** @var array $pending */ | ||||||
|  | /** @var array $applied */ | ||||||
|  | /** @var string $csrf_token */ | ||||||
|  | ?>
 | ||||||
|  | <!-- admin tools page --> | ||||||
|  | <div class="container-fluid mt-2"> | ||||||
|  |     <div class="row mb-4"> | ||||||
|  |         <div class="col-12 mb-2"> | ||||||
|  |             <h2>Admin tools</h2> | ||||||
|  |             <small class="text-muted">System maintenance and database utilities.</small> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div class="row g-4"> | ||||||
|  |         <div class="col-lg-6"> | ||||||
|  |             <div class="card shadow-sm"> | ||||||
|  |                 <div class="card-header bg-light d-flex justify-content-between align-items-center py-3"> | ||||||
|  |                     <h5 class="card-title mb-0"> | ||||||
|  |                         <i class="fas fa-tools me-2 text-secondary"></i> | ||||||
|  |                         Maintenance mode | ||||||
|  |                     </h5> | ||||||
|  |                     <span class="badge <?= $maintenance_enabled ? 'bg-danger' : 'bg-success' ?>"><?= $maintenance_enabled ? 'enabled' : 'disabled' ?></span>
 | ||||||
|  |                 </div> | ||||||
|  |                 <div class="card-body p-4"> | ||||||
|  |                     <form method="post" class="mb-3"> | ||||||
|  |                         <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>"> | ||||||
|  |                         <input type="hidden" name="action" value="maintenance_on"> | ||||||
|  |                         <div class="mb-3"> | ||||||
|  |                             <label for="maintenance_message" class="form-label mb-1">Message (optional)</label> | ||||||
|  |                             <input type="text" id="maintenance_message" name="maintenance_message" class="form-control form-control-sm" value="<?= htmlspecialchars($maintenance_message) ?>" placeholder="Upgrading database"> | ||||||
|  |                         </div> | ||||||
|  |                         <button type="submit" class="btn btn-warning btn-sm" <?= $maintenance_enabled ? 'disabled' : '' ?>>Enable maintenance</button>
 | ||||||
|  |                     </form> | ||||||
|  |                     <form method="post" class="mt-2"> | ||||||
|  |                         <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>"> | ||||||
|  |                         <input type="hidden" name="action" value="maintenance_off"> | ||||||
|  |                         <button type="submit" class="btn btn-outline-secondary btn-sm" <?= $maintenance_enabled ? '' : 'disabled' ?>>Disable maintenance</button>
 | ||||||
|  |                     </form> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <div class="col-lg-6"> | ||||||
|  |             <div class="card shadow-sm"> | ||||||
|  |                 <div class="card-header bg-light d-flex justify-content-between align-items-center py-3"> | ||||||
|  |                     <h5 class="card-title mb-0"> | ||||||
|  |                         <i class="fas fa-database me-2 text-secondary"></i> | ||||||
|  |                         Database migrations | ||||||
|  |                     </h5> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="card-body p-4"> | ||||||
|  |                     <?php if (!empty($migration_error)): ?>
 | ||||||
|  |                         <div class="alert alert-danger">Error: <?= htmlspecialchars($migration_error) ?></div>
 | ||||||
|  |                     <?php endif; ?>
 | ||||||
|  |                     <div class="mb-3"> | ||||||
|  |                         <div class="d-flex justify-content-between align-items-center"> | ||||||
|  |                             <div><strong>Pending</strong></div> | ||||||
|  |                             <span class="badge <?= empty($pending) ? 'bg-success' : 'bg-warning text-dark' ?>"><?= count($pending) ?></span>
 | ||||||
|  |                         </div> | ||||||
|  |                         <div class="mt-2 small border rounded" style="max-height: 240px; overflow: auto;"> | ||||||
|  |                             <?php if (empty($pending)): ?>
 | ||||||
|  |                                 <div class="p-2"><span class="text-success">none</span></div> | ||||||
|  |                             <?php else: ?>
 | ||||||
|  |                                 <ul class="list-group list-group-flush"> | ||||||
|  |                                     <?php foreach ($pending as $fname): ?>
 | ||||||
|  |                                         <li class="list-group-item d-flex justify-content-between align-items-center"> | ||||||
|  |                                             <span class="text-monospace small"><?= htmlspecialchars($fname) ?></span>
 | ||||||
|  |                                             <button type="button" | ||||||
|  |                                                     class="btn btn-outline-primary btn-sm" | ||||||
|  |                                                     data-toggle="modal" | ||||||
|  |                                                     data-target="#migrationModal<?= md5($fname) ?>">View | ||||||
|  |                                             </button> | ||||||
|  |                                         </li> | ||||||
|  |                                     <?php endforeach; ?>
 | ||||||
|  |                                 </ul> | ||||||
|  |                             <?php endif; ?>
 | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="mb-3"> | ||||||
|  |                         <div class="d-flex justify-content-between align-items-center"> | ||||||
|  |                             <div><strong>Applied</strong></div> | ||||||
|  |                             <span class="badge bg-secondary"><?= count($applied) ?></span>
 | ||||||
|  |                         </div> | ||||||
|  |                         <div class="mt-2 small border rounded" style="max-height: 240px; overflow: auto;"> | ||||||
|  |                             <?php if (empty($applied)): ?>
 | ||||||
|  |                                 <div class="p-2"><span class="text-muted">none</span></div> | ||||||
|  |                             <?php else: ?>
 | ||||||
|  |                                 <ul class="list-group list-group-flush"> | ||||||
|  |                                     <?php foreach ($applied as $fname): ?>
 | ||||||
|  |                                         <li class="list-group-item d-flex justify-content-between align-items-center"> | ||||||
|  |                                             <span class="text-monospace small"><?= htmlspecialchars($fname) ?></span>
 | ||||||
|  |                                             <button type="button" | ||||||
|  |                                                     class="btn btn-outline-secondary btn-sm" | ||||||
|  |                                                     data-toggle="modal" | ||||||
|  |                                                     data-target="#migrationModal<?= md5($fname) ?>">View | ||||||
|  |                                             </button> | ||||||
|  |                                         </li> | ||||||
|  |                                     <?php endforeach; ?>
 | ||||||
|  |                                 </ul> | ||||||
|  |                             <?php endif; ?>
 | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                     <form method="post" class="mt-3"> | ||||||
|  |                         <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf_token) ?>"> | ||||||
|  |                         <input type="hidden" name="action" value="migrate_up"> | ||||||
|  |                         <button type="submit" class="btn btn-primary btn-sm" <?= empty($pending) ? 'disabled' : '' ?>>Apply pending migrations</button>
 | ||||||
|  |                     </form> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <!-- Migration viewer modals (one per file) --> | ||||||
|  | <?php if (!empty($migration_contents)): | ||||||
|  |       foreach ($migration_contents as $name => $content): | ||||||
|  |           $modalId = 'migrationModal' . md5($name); | ||||||
|  | ?>
 | ||||||
|  | <div class="modal fade" id="<?= $modalId ?>" tabindex="-1" aria-labelledby="<?= $modalId ?>Label" aria-hidden="true"> | ||||||
|  |     <div class="modal-dialog modal-lg"> | ||||||
|  |         <div class="modal-content"> | ||||||
|  |             <div class="modal-header"> | ||||||
|  |                 <h5 class="modal-title" id="<?= $modalId ?>Label"><?= htmlspecialchars($name) ?></h5>
 | ||||||
|  |                 <button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button> | ||||||
|  |             </div> | ||||||
|  |             <div class="modal-body p-0"> | ||||||
|  |                 <pre class="mb-0" style="max-height: 60vh; overflow: auto;"><code class="p-3 d-block"><?= htmlspecialchars($content) ?></code></pre>
 | ||||||
|  |             </div> | ||||||
|  |             <div class="modal-footer"> | ||||||
|  |                 <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | <?php   endforeach; | ||||||
|  |       endif; ?>
 | ||||||
|  | @ -1,39 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- jilo agents --> |  | ||||||
|                 <div class="card text-center w-75 mx-lef"> |  | ||||||
|                     <p class="h4 card-header">Jilo Agents on platform <?= htmlspecialchars($platform_id) ?> (<?= htmlspecialchars($platformDetails[0]['name']) ?>)</p>
 |  | ||||||
|                     <div class="card-body"> |  | ||||||
| <?php foreach ($agentDetails as $agent) { ?>
 |  | ||||||
|                         <p class="card-text text-left" style="text-align: left;"> |  | ||||||
|                             agent id: <strong><?= htmlspecialchars($agent['id']) ?></strong>
 |  | ||||||
|                             agent type: <?= htmlspecialchars($agent['agent_type_id']) ?> (<strong><?= htmlspecialchars($agent['agent_description']) ?></strong>)
 |  | ||||||
|                             <br /> |  | ||||||
|                             endpoint: <strong><?= htmlspecialchars($agent['url']) ?><?= htmlspecialchars($agent['agent_endpoint']) ?></strong>
 |  | ||||||
|                             <br /> |  | ||||||
| <?php |  | ||||||
|     $payload = [ |  | ||||||
|         'iss' => 'Jilo Web', |  | ||||||
|         'aud' => $config['domain'], |  | ||||||
|         'iat' => time(), |  | ||||||
|         'exp' => time() + 3600, |  | ||||||
|         'agent_id' => $agent['id'] |  | ||||||
|     ]; |  | ||||||
|     $jwt = $agentObject->generateAgentToken($payload, $agent['secret_key']); |  | ||||||
| //    print_r($_SESSION);
 |  | ||||||
| ?>
 |  | ||||||
| <?php if (isset($_SESSION["agent{$agent['id']}_cache"])) { ?>
 |  | ||||||
|                             <button id="agent<?= htmlspecialchars($agent['id']) ?>-status" class="btn btn-primary" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="get the agent status" onclick="fetchData('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['url']) ?>', '/status', '<?= htmlspecialchars($jwt) ?>', true)">get status</button> |  | ||||||
|                             <button id="agent<?= htmlspecialchars($agent['id']) ?>-fetch" class="btn btn-primary" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="get data from the agent" onclick="fetchData('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['url']) ?>', '<?= htmlspecialchars($agent['agent_endpoint']) ?>', '<?= htmlspecialchars($jwt) ?>', true)">fetch data</button> |  | ||||||
|                             <button id="agent<?= htmlspecialchars($agent['id']) ?>-cache" class="btn btn-secondary" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="load cache" onclick="loadCache('<?= htmlspecialchars($agent['id']) ?>')">load cache</button> |  | ||||||
|                             <button id="agent<?= htmlspecialchars($agent['id']) ?>-clear" class="btn btn-danger" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="clear cache" onclick="clearCache('<?= htmlspecialchars($agent['id']) ?>')">clear cache</button> |  | ||||||
|                             <span id="cacheInfo<?= htmlspecialchars($agent['id']) ?>" style="margin: 5px 0;"></span> |  | ||||||
| <?php } else { ?>
 |  | ||||||
|                             <button id="agent<?= htmlspecialchars($agent['id']) ?>-status" class="btn btn-primary" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="get the agent status" onclick="fetchData('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['url']) ?>', '/status', '<?= htmlspecialchars($jwt) ?>', true)">get status</button> |  | ||||||
|                             <button id="agent<?= htmlspecialchars($agent['id']) ?>-fetch" class="btn btn-primary" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="get data from the agent" onclick="fetchData('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['url']) ?>', '<?= htmlspecialchars($agent['agent_endpoint']) ?>', '<?= htmlspecialchars($jwt) ?>')">fetch data</button> |  | ||||||
|                             <button style="display: none" disabled id="agent<?= htmlspecialchars($agent['id']) ?>-cache" class="btn btn-secondary" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="load cache" onclick="loadCache('<?= htmlspecialchars($agent['id']) ?>')">load cache</button> |  | ||||||
|                             <button style="display: none" disabled id="agent<?= htmlspecialchars($agent['id']) ?>-clear" class="btn btn-danger" data-toggle="tooltip" data-trigger="hover" data-placement="bottom" title="clear cache" onclick="clearCache('<?= htmlspecialchars($agent['id']) ?>')">clear cache</button> |  | ||||||
|                             <span style="display: none" id="cacheInfo<?= htmlspecialchars($agent['id']) ?>" style="margin: 5px 0;"></span> |  | ||||||
| <?php } ?>
 |  | ||||||
|                     </p> |  | ||||||
|                         <pre class="results" id="result<?= htmlspecialchars($agent['id']) ?>">click a button to display data from the agent.</pre> |  | ||||||
| <?php } ?>
 |  | ||||||
|  | @ -0,0 +1,94 @@ | ||||||
|  | 
 | ||||||
|  |                 <!-- agents live data --> | ||||||
|  |                 <div class="container-fluid mt-2"> | ||||||
|  |                     <div class="row mb-4"> | ||||||
|  |                         <div class="col-12 mb-4"> | ||||||
|  |                             <h2 class="mb-0">Jilo Agents status</h2> | ||||||
|  |                             <small>manage and monitor agents on platform <strong><?= htmlspecialchars($platformDetails[0]['name']) ?></strong></small>
 | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  | 
 | ||||||
|  |                     <!-- hosts and their agents --> | ||||||
|  |                     <div class="row"> | ||||||
|  |                         <?php foreach ($agentsByHost as $hostId => $hostData): ?>
 | ||||||
|  |                             <div class="col-12 mb-4"> | ||||||
|  |                                 <div class="card"> | ||||||
|  |                                     <div class="card-header bg-light"> | ||||||
|  |                                         <h5 class="mb-0"> | ||||||
|  |                                             <i class="fas fa-network-wired me-2 text-secondary"></i> | ||||||
|  |                                             Host: <?= htmlspecialchars($hostData['host_name']) ?>
 | ||||||
|  |                                             <a href="<?= htmlspecialchars($app_root) ?>?page=settings#platform-<?= htmlspecialchars($platform_id) ?>host-<?= htmlspecialchars($hostId) ?>" class="text-decoration-none"> | ||||||
|  |                                                 <i class="fas fa-edit ms-2"></i> | ||||||
|  |                                             </a> | ||||||
|  |                                         </h5> | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class="card-body"> | ||||||
|  |                                         <?php if (empty($hostData['agents'])): ?>
 | ||||||
|  |                                             <p class="text-muted">No agents on this host.</p> | ||||||
|  |                                         <?php else: ?>
 | ||||||
|  |                                             <?php foreach ($hostData['agents'] as $agent): ?>
 | ||||||
|  |                                                 <div class="agent-item mb-4 pb-3 border-bottom"> | ||||||
|  |                                                     <div class="d-flex align-items-center mb-2"> | ||||||
|  |                                                         <div class="flex-grow-1"> | ||||||
|  |                                                             <i class="fas fa-robot me-2 text-secondary"></i> | ||||||
|  |                                                             <strong>Agent ID:</strong> <?= htmlspecialchars($agent['id']) ?> |
 | ||||||
|  |                                                             <strong>Type:</strong> <?= htmlspecialchars($agent['agent_type_id']) ?> (<?= htmlspecialchars($agent['agent_description']) ?>) |
 | ||||||
|  |                                                             <strong>Endpoint:</strong> <?= htmlspecialchars($agent['url']) ?><?= htmlspecialchars($agent['agent_endpoint']) ?>
 | ||||||
|  |                                                             <a href="<?= htmlspecialchars($app_root) ?>?page=settings#platform-<?= htmlspecialchars($platform_id) ?>agent-<?= htmlspecialchars($agent['id']) ?>" class="text-decoration-none"> | ||||||
|  |                                                                 <i class="fas fa-edit ms-2"></i> | ||||||
|  |                                                             </a> | ||||||
|  |                                                         </div> | ||||||
|  |                                                     </div> | ||||||
|  | 
 | ||||||
|  |                                                     <div class="btn-group" role="group"> | ||||||
|  |                                                         <button id="agent<?= htmlspecialchars($agent['id']) ?>-status"  | ||||||
|  |                                                                 class="btn btn-primary"  | ||||||
|  |                                                                 data-toggle="tooltip"  | ||||||
|  |                                                                 data-trigger="hover"  | ||||||
|  |                                                                 data-placement="bottom"  | ||||||
|  |                                                                 title="Get the agent status"  | ||||||
|  |                                                                 onclick="fetchData('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['url']) ?>', '/status', '<?= htmlspecialchars($agentTokens[$agent['id']]) ?>', true)"> | ||||||
|  |                                                             Get Status | ||||||
|  |                                                         </button> | ||||||
|  |                                                         <button id="agent<?= htmlspecialchars($agent['id']) ?>-fetch"  | ||||||
|  |                                                                 class="btn btn-primary"  | ||||||
|  |                                                                 data-toggle="tooltip"  | ||||||
|  |                                                                 data-trigger="hover"  | ||||||
|  |                                                                 data-placement="bottom"  | ||||||
|  |                                                                 title="Get data from the agent"  | ||||||
|  |                                                                 onclick="fetchData('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['url']) ?>', '<?= htmlspecialchars($agent['agent_endpoint']) ?>', '<?= htmlspecialchars($agentTokens[$agent['id']]) ?>', <?= isset($_SESSION["agent{$agent['id']}_cache"]) ? 'true' : 'false' ?>)">
 | ||||||
|  |                                                             Fetch Data | ||||||
|  |                                                         </button> | ||||||
|  |                                                         <button id="agent<?= htmlspecialchars($agent['id']) ?>-cache"  | ||||||
|  |                                                                 <?= !isset($_SESSION["agent{$agent['id']}_cache"]) ? 'style="display:none;" ' : '' ?>
 | ||||||
|  |                                                                 class="btn btn-secondary"  | ||||||
|  |                                                                 data-toggle="tooltip"  | ||||||
|  |                                                                 data-trigger="hover"  | ||||||
|  |                                                                 data-placement="bottom"  | ||||||
|  |                                                                 title="Load cache"  | ||||||
|  |                                                                 onclick="loadCache('<?= htmlspecialchars($agent['id']) ?>')"> | ||||||
|  |                                                             Load Cache | ||||||
|  |                                                         </button> | ||||||
|  |                                                         <button id="agent<?= htmlspecialchars($agent['id']) ?>-clear"  | ||||||
|  |                                                                 <?= !isset($_SESSION["agent{$agent['id']}_cache"]) ? 'style="display:none;" ' : '' ?>
 | ||||||
|  |                                                                 class="btn btn-danger"  | ||||||
|  |                                                                 data-toggle="tooltip"  | ||||||
|  |                                                                 data-trigger="hover"  | ||||||
|  |                                                                 data-placement="bottom"  | ||||||
|  |                                                                 title="Clear cache"  | ||||||
|  |                                                                 onclick="clearCache('<?= htmlspecialchars($agent['id']) ?>')"> | ||||||
|  |                                                             Clear Cache | ||||||
|  |                                                         </button> | ||||||
|  |                                                     </div> | ||||||
|  |                                                     <span id="cacheInfo<?= htmlspecialchars($agent['id']) ?>" class="ms-2 <?= isset($_SESSION["agent{$agent['id']}_cache"]) ? '' : 'd-none' ?>"></span>
 | ||||||
|  |                                                     <pre class="results mt-3" id="result<?= htmlspecialchars($agent['id']) ?>">Click a button to display data from the agent.</pre> | ||||||
|  |                                                 </div> | ||||||
|  |                                             <?php endforeach; ?>
 | ||||||
|  |                                         <?php endif; ?>
 | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                         <?php endforeach; ?>
 | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |                 <!-- agents live data --> | ||||||
|  | @ -0,0 +1,110 @@ | ||||||
|  | 
 | ||||||
|  |                 <!-- jitsi components events --> | ||||||
|  |                 <div class="container-fluid mt-2"> | ||||||
|  |                     <div class="row mb-4"> | ||||||
|  |                         <div class="col-md-6 mb-5"> | ||||||
|  |                             <h2 class="mb-0">Jitsi components events</h2> | ||||||
|  |                             <small>log events related to Jitsi Meet components like Jicofo, Videobridge, Jigasi, etc.</small> | ||||||
|  |                         </div> | ||||||
|  |                         <div class="row mb-4"> | ||||||
|  | 
 | ||||||
|  |                             <!-- component events filter --> | ||||||
|  |                             <div class="card mb-3"> | ||||||
|  |                                 <div class="card-body"> | ||||||
|  |                                     <form method="get" action="" class="row g-3 align-items-end"> | ||||||
|  |                                         <input type="hidden" name="page" value="components"> | ||||||
|  |                                         <div class="col-md-auto"> | ||||||
|  |                                             <label for="from_time" class="form-label">From date</label> | ||||||
|  |                                             <input type="date" class="form-control" id="from_time" name="from_time" value="<?= htmlspecialchars($_REQUEST['from_time'] ?? '') ?>"> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="col-md-auto"> | ||||||
|  |                                             <label for="until_time" class="form-label">Until date</label> | ||||||
|  |                                             <input type="date" class="form-control" id="until_time" name="until_time" value="<?= htmlspecialchars($_REQUEST['until_time'] ?? '') ?>"> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="col-md-2"> | ||||||
|  |                                             <label for="name" class="form-label">Component name</label> | ||||||
|  |                                             <input type="text" class="form-control" id="name" name="name" value="<?= htmlspecialchars($_REQUEST['name'] ?? '') ?>" placeholder="Component name"> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="col-md-4"> | ||||||
|  |                                             <input type="text" class="form-control" id="id" name="id" value="<?= htmlspecialchars($_REQUEST['id'] ?? '') ?>" placeholder="Search in component IDs"> | ||||||
|  |                                             <input type="text" class="form-control" id="event" name="event" value="<?= htmlspecialchars($_REQUEST['event'] ?? '') ?>" placeholder="Search in event messages"> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="col-md-auto align-middle"> | ||||||
|  |                                             <button type="submit" class="btn btn-primary me-2"> | ||||||
|  |                                                 <i class="fas fa-search me-2"></i>Search | ||||||
|  |                                             </button> | ||||||
|  |                                             <a href="?page=components" class="btn btn-outline-secondary"> | ||||||
|  |                                                 <i class="fas fa-times me-2"></i>Clear | ||||||
|  |                                             </a> | ||||||
|  |                                         </div> | ||||||
|  |                                     </form> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                             <!-- /component events filter --> | ||||||
|  | 
 | ||||||
|  |                             <!-- component events --> | ||||||
|  | <?php if ($time_range_specified || count($filterMessage)) { ?>
 | ||||||
|  |                             <div class="alert alert-info m-0 mb-3 small"> | ||||||
|  | <?php   if ($time_range_specified) { ?>
 | ||||||
|  |                               <p class="mb-0"><i class="fas fa-calendar-alt me-2"></i>Time period: | ||||||
|  |                                   <strong> | ||||||
|  |                                       <?= $from_time == '0000-01-01' ? 'beginning' : date('d M Y', strtotime($from_time)) ?> - <?= $until_time == '9999-12-31' ? 'now' : date('d M Y', strtotime($until_time)) ?>
 | ||||||
|  |                                   </strong> | ||||||
|  |                               </p> | ||||||
|  | <?php   } ?>
 | ||||||
|  | <?php   if (count($filterMessage)) { | ||||||
|  |           foreach ($filterMessage as $message) { ?>
 | ||||||
|  |                                 <p class="mb-0"><i class="fas fa-users me-2"></i><?= $message ?></strong></p>
 | ||||||
|  | <?php     } ?>
 | ||||||
|  | <?php   } ?>
 | ||||||
|  |                             </div> | ||||||
|  | <?php } ?>
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |                             <div class="mb-5"> | ||||||
|  | <?php if (!empty($components['records'])) { ?>
 | ||||||
|  |                                 <div class="table-responsive border"> | ||||||
|  |                                     <table class="table table-results table-hover mb-0"> | ||||||
|  |                                         <thead class="table-light"> | ||||||
|  |                                             <tr> | ||||||
|  |                                                 <th>component</th> | ||||||
|  |                                                 <th>log level</th> | ||||||
|  |                                                 <th>time</th> | ||||||
|  |                                                 <th>component ID</th> | ||||||
|  |                                                 <th>event</th> | ||||||
|  |                                                 <th>parameter</th> | ||||||
|  |                                             </tr> | ||||||
|  |                                         </thead> | ||||||
|  |                                         <tbody> | ||||||
|  | <?php     foreach ($components['records'] as $row) { ?>
 | ||||||
|  |                                             <tr> | ||||||
|  |                                                 <td> | ||||||
|  |                                                     <a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=components&name=<?= htmlspecialchars($row['component'] ?? '') ?>"> | ||||||
|  |                                                         <?= htmlspecialchars($row['component'] ?? '') ?>
 | ||||||
|  |                                                     </a> | ||||||
|  |                                                 </td> | ||||||
|  |                                                 <td><?= htmlspecialchars($row['loglevel']) ?></td>
 | ||||||
|  |                                                 <td><span class="text-muted"><?= date('d M Y H:i:s', strtotime($row['time'])) ?></span></td>
 | ||||||
|  |                                                 <td> | ||||||
|  |                                                     <a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=components&id=<?= htmlspecialchars($row['component ID'] ?? '') ?>"> | ||||||
|  |                                                         <?= htmlspecialchars($row['component ID'] ?? '') ?>
 | ||||||
|  |                                                     </a> | ||||||
|  |                                                 </td> | ||||||
|  |                                                 <td><?= htmlspecialchars($row['event']) ?></td>
 | ||||||
|  |                                                 <td><?= htmlspecialchars($row['param']) ?></td>
 | ||||||
|  |                                             </tr> | ||||||
|  | <?php     } ?>
 | ||||||
|  |                                         </tbody> | ||||||
|  |                                     </table> | ||||||
|  |                                 </div> | ||||||
|  | <?php include '../app/templates/pagination.php'; ?>
 | ||||||
|  | <?php } else { ?>
 | ||||||
|  |                                 <div class="alert alert-danger m-0"> | ||||||
|  |                                     <i class="fas fa-info-circle me-2"></i>No component events found for the specified criteria. | ||||||
|  |                                 </div> | ||||||
|  | <?php } ?>
 | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |                 <!-- /jitsi components events --> | ||||||
|  | @ -0,0 +1,145 @@ | ||||||
|  | 
 | ||||||
|  |                 <!-- jitsi conferences events --> | ||||||
|  |                 <div class="container-fluid mt-2"> | ||||||
|  |                     <div class="row mb-4"> | ||||||
|  |                         <div class="col-md-6 mb-5"> | ||||||
|  |                             <h2 class="mb-0">Jitsi conferences events</h2> | ||||||
|  |                             <small>log events related to conferences in Jitsi Meet</small> | ||||||
|  |                         </div> | ||||||
|  |                         <div class="row mb-4"> | ||||||
|  | 
 | ||||||
|  |                             <!-- conference events filter --> | ||||||
|  |                             <div class="card mb-3"> | ||||||
|  |                                 <div class="card-body"> | ||||||
|  |                                     <form method="get" action="" class="row g-3 align-items-end"> | ||||||
|  |                                         <input type="hidden" name="page" value="conferences"> | ||||||
|  |                                         <div class="col-md-auto"> | ||||||
|  |                                             <label for="from_time" class="form-label">From date</label> | ||||||
|  |                                             <input type="date" class="form-control" id="from_time" name="from_time" value="<?= htmlspecialchars($_REQUEST['from_time'] ?? '') ?>"> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="col-md-auto"> | ||||||
|  |                                             <label for="until_time" class="form-label">Until date</label> | ||||||
|  |                                             <input type="date" class="form-control" id="until_time" name="until_time" value="<?= htmlspecialchars($_REQUEST['until_time'] ?? '') ?>"> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="col-md-2"> | ||||||
|  |                                             <label for="name" class="form-label">Conference ID</label> | ||||||
|  |                                             <input type="text" class="form-control" id="id" name="name" value="<?= htmlspecialchars($_REQUEST['id'] ?? '') ?>" placeholder="Conference ID"> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="col-md-4"> | ||||||
|  |                                             <label for="name" class="form-label">Conference name</label> | ||||||
|  |                                             <input type="text" class="form-control" id="name" name="name" value="<?= htmlspecialchars($_REQUEST['name'] ?? '') ?>" placeholder="Search in conference names"> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="col-md-auto align-middle"> | ||||||
|  |                                             <button type="submit" class="btn btn-primary me-2"> | ||||||
|  |                                                 <i class="fas fa-search me-2"></i>Search | ||||||
|  |                                             </button> | ||||||
|  |                                             <a href="?page=conferences" class="btn btn-outline-secondary"> | ||||||
|  |                                                 <i class="fas fa-times me-2"></i>Clear | ||||||
|  |                                             </a> | ||||||
|  |                                         </div> | ||||||
|  |                                     </form> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                             <!-- /conference events filter --> | ||||||
|  | 
 | ||||||
|  |                             <!-- conference events --> | ||||||
|  | <?php if ($time_range_specified || count($filterMessage)) { ?>
 | ||||||
|  |                             <div class="alert alert-info m-0 mb-3 small"> | ||||||
|  | <?php   if ($time_range_specified) { ?>
 | ||||||
|  |                                 <p class="mb-0"><i class="fas fa-calendar-alt me-2"></i>Time period: | ||||||
|  |                                     <strong> | ||||||
|  |                                         <?= $from_time == '0000-01-01' ? 'beginning' : date('d M Y', strtotime($from_time)) ?> - <?= $until_time == '9999-12-31' ? 'now' : date('d M Y', strtotime($until_time)) ?>
 | ||||||
|  |                                     </strong> | ||||||
|  |                                 </p> | ||||||
|  | <?php   } ?>
 | ||||||
|  | <?php   if (count($filterMessage)) { | ||||||
|  |           foreach ($filterMessage as $message) { ?>
 | ||||||
|  |                                 <p class="mb-0"><i class="fas fa-users me-2"></i><?= $message ?></strong></p>
 | ||||||
|  | <?php     } ?>
 | ||||||
|  | <?php   } ?>
 | ||||||
|  |                             </div> | ||||||
|  | <?php } ?>
 | ||||||
|  | 
 | ||||||
|  |                             <div class="mb-5"> | ||||||
|  | <?php if (!empty($conferences['records'])) { ?>
 | ||||||
|  |                                 <div class="table-responsive border"> | ||||||
|  |                                     <table class="table table-results table-hover"> | ||||||
|  |                                         <thead class="table-light"> | ||||||
|  |                                             <tr> | ||||||
|  | <?php     foreach (array_keys($conferences['records'][0]) as $header) { ?>
 | ||||||
|  |                                                 <th scope="col" class="text-nowrap"><?= htmlspecialchars($header) ?></th>
 | ||||||
|  | <?php     } ?>
 | ||||||
|  |                                             </tr> | ||||||
|  |                                         </thead> | ||||||
|  |                                         <tbody> | ||||||
|  | <?php     foreach ($conferences['records'] as $row) { ?>
 | ||||||
|  |                                             <tr> | ||||||
|  | <?php       foreach ($row as $key => $column) { | ||||||
|  |                     if ($key === 'conference ID' && isset($conferenceId) && $conferenceId === $column) { ?>
 | ||||||
|  |                                                 <td class="text-nowrap"> | ||||||
|  |                                                     <strong <?= (strlen($column ?? '') > 20) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
 | ||||||
|  |                                                         <?= htmlspecialchars(strlen($column ?? '') > 20 ? substr($column, 0, 20) . '...' : $column ?? '') ?>
 | ||||||
|  |                                                     </strong> | ||||||
|  |                                                 </td> | ||||||
|  | <?php               } elseif ($key === 'conference ID') { ?>
 | ||||||
|  |                                                 <td class="text-nowrap"> | ||||||
|  |                                                     <a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=conferences&id=<?= htmlspecialchars($column ?? '') ?>" | ||||||
|  |                                                        <?= (strlen($column ?? '') > 16) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
 | ||||||
|  |                                                         <?= htmlspecialchars(strlen($column ?? '') > 16 ? substr($column, 0, 16) . '...' : $column ?? '') ?>
 | ||||||
|  |                                                     </a> | ||||||
|  |                                                 </td> | ||||||
|  | <?php               } elseif ($key === 'conference name' && isset($conferenceName) && $conferenceName === $column) { ?>
 | ||||||
|  |                                                 <td class="text-nowrap"> | ||||||
|  |                                                     <strong <?= (strlen($column ?? '') > 20) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
 | ||||||
|  |                                                         <?= htmlspecialchars(strlen($column ?? '') > 20 ? substr($column, 0, 20) . '...' : $column ?? '') ?>
 | ||||||
|  |                                                     </strong> | ||||||
|  |                                                 </td> | ||||||
|  | <?php               } elseif ($key === 'conference name') { ?>
 | ||||||
|  |                                                 <td class="text-nowrap"> | ||||||
|  |                                                     <a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=conferences&name=<?= htmlspecialchars($column ?? '') ?>" | ||||||
|  |                                                        <?= (strlen($column ?? '') > 16) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
 | ||||||
|  |                                                         <?= htmlspecialchars(strlen($column ?? '') > 16 ? substr($column, 0, 16) . '...' : $column ?? '') ?>
 | ||||||
|  |                                                     </a> | ||||||
|  |                                                 </td> | ||||||
|  | <?php               } elseif ($key === 'conference host') { ?>
 | ||||||
|  |                                                 <td class="text-nowrap"> | ||||||
|  |                                                     <span <?= (strlen($column ?? '') > 30) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
 | ||||||
|  |                                                         <?= htmlspecialchars(strlen($column ?? '') > 30 ? substr($column, 0, 30) . '...' : $column ?? '') ?>
 | ||||||
|  |                                                     </span> | ||||||
|  |                                                 </td> | ||||||
|  | <?php               } elseif ($key === 'participant ID') { ?>
 | ||||||
|  |                                                 <td class="text-nowrap"> | ||||||
|  |                                                     <a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=participants&id=<?= htmlspecialchars($column ?? '') ?>" | ||||||
|  |                                                        <?= (strlen($column ?? '') > 16) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
 | ||||||
|  |                                                         <?= htmlspecialchars(strlen($column ?? '') > 16 ? substr($column, 0, 16) . '...' : $column ?? '') ?>
 | ||||||
|  |                                                     </a> | ||||||
|  |                                                 </td> | ||||||
|  | <?php               } elseif ($key === 'component') { ?>
 | ||||||
|  |                                                 <td class="text-nowrap"> | ||||||
|  |                                                     <a href="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=components&name=<?= htmlspecialchars($column ?? '') ?>" | ||||||
|  |                                                        <?= (strlen($column ?? '') > 16) ? 'data-toggle="tooltip" title="' . htmlspecialchars($column) . '"' : '' ?>>
 | ||||||
|  |                                                         <?= htmlspecialchars(strlen($column ?? '') > 16 ? substr($column, 0, 16) . '...' : $column ?? '') ?>
 | ||||||
|  |                                                     </a> | ||||||
|  |                                                 </td> | ||||||
|  | <?php               } elseif ($key === 'time' || $key === 'start' || $key === 'end') { ?>
 | ||||||
|  |                                                 <td class="text-nowrap"><?= !empty($column) ? date('d M Y H:i:s',strtotime($column)) : '<small class="text-muted">n/a</small>' ?></td>
 | ||||||
|  | <?php               } else { ?>
 | ||||||
|  |                                                 <td><?= htmlspecialchars($column ?? '') ?></td>
 | ||||||
|  | <?php               } | ||||||
|  |             } ?>
 | ||||||
|  |                                             </tr> | ||||||
|  | <?php     } ?>
 | ||||||
|  |                                         </tbody> | ||||||
|  |                                     </table> | ||||||
|  |                                 </div> | ||||||
|  | <?php include '../app/templates/pagination.php'; ?>
 | ||||||
|  | <?php } else { ?>
 | ||||||
|  |                                 <div class="alert alert-danger m-0"> | ||||||
|  |                                     <i class="fas fa-info-circle me-2"></i>No conference events found for the specified criteria. | ||||||
|  |                                 </div> | ||||||
|  | <?php } ?>
 | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |                 <!-- /jitsi conferences events --> | ||||||
|  | @ -1,72 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "agents" --> |  | ||||||
|                 <div class="card text-center w-50 mx-auto"> |  | ||||||
|                     <p class="h4 card-header">Add new Jilo Agent to Jitsi platform "<strong><?= htmlspecialchars($platformDetails[0]['name']) ?></strong>"</p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <!--p class="card-text">add new agent:</p--> |  | ||||||
|                         <form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config"> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="type" class="form-label">type</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <select class="form-control" type="text" name="type" id="agent_type_id" required> |  | ||||||
|                                         <option></option> |  | ||||||
| <?php foreach ($jilo_agent_types as $agent_type) { ?>
 |  | ||||||
|                                         <option value="<?= htmlspecialchars($agent_type['id']) ?>"<?php |  | ||||||
| if (in_array($agent_type['id'], $jilo_agent_types_in_platform)) { |  | ||||||
|     echo 'disabled="disabled"'; |  | ||||||
| } ?>>
 |  | ||||||
|                                             <?= htmlspecialchars($agent_type['description']) ?>
 |  | ||||||
|                                         </option> |  | ||||||
| <?php } ?>
 |  | ||||||
|                                     </select> |  | ||||||
|                                     <p class="text-start"><small>type of agent (meet, jvb, jibri, etc.)<br />if a type has already been aded, it's disabled here</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="url" class="form-label">URL</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="url" value="https://" required /> |  | ||||||
|                                     <p class="text-start"><small>URL of the Jilo Agent API (https://example.com:8081)</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="secret_key" class="form-label">secret key</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="secret_key" value="" required /> |  | ||||||
|                                     <p class="text-start"><small>secret key for generating the access JWT token</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="check_period" class="form-label">check period</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="check_period" value="0" required /> |  | ||||||
|                                     <p class="text-start"><small>period in minutes for the automatic agent check (0 disables it)</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <input type="hidden" name="new" value="true" /> |  | ||||||
|                             <input type="hidden" name="item" value="agent" /> |  | ||||||
| 
 |  | ||||||
|                             <br /> |  | ||||||
|                             <a class="btn btn-secondary" href="<?= htmlspecialchars($app_root) ?>?page=config" />Cancel</a> |  | ||||||
|                             <input type="submit" class="btn btn-primary" value="Save" /> |  | ||||||
|                         </form> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "agents" --> |  | ||||||
|  | @ -1,27 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "config file" --> |  | ||||||
|                 <div class="card text-center w-75 mx-lef"> |  | ||||||
|                     <p class="h4 card-header">Jilo configuration file :: edit</p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <div class="card-text"> |  | ||||||
|                             <p class="text-danger"><strong>this may break everything, use with extreme caution</strong></p> |  | ||||||
|                         </div> |  | ||||||
|                         <form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=config&item=config_file"> |  | ||||||
| 
 |  | ||||||
| <?php |  | ||||||
| include '../app/helpers/render.php'; |  | ||||||
| editConfig($config, '0'); |  | ||||||
| echo "\n"; |  | ||||||
| ?>
 |  | ||||||
| 
 |  | ||||||
|                             <p class="text-danger"><strong>this may break everything, use with extreme caution</strong></p> |  | ||||||
|                             <br /> |  | ||||||
|                             <input type="hidden" name="item" value="config_file" /> |  | ||||||
|                             <a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=config_file" />Cancel</a> |  | ||||||
|                                |  | ||||||
|                             <input type="submit" class="btn btn-danger btn-sm" value="Save" /> |  | ||||||
|                         </form> |  | ||||||
| 
 |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "config file" --> |  | ||||||
|  | @ -1,17 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "config file" --> |  | ||||||
|                 <div class="card text-center w-75 mx-lef"> |  | ||||||
|                     <p class="h4 card-header">Jilo configuration file</p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
| 
 |  | ||||||
| <?php |  | ||||||
| include '../app/helpers/render.php'; |  | ||||||
| renderConfig($config, '0'); |  | ||||||
| echo "\n"; |  | ||||||
| ?>
 |  | ||||||
| 
 |  | ||||||
|                         <br /> |  | ||||||
|                         <a class="btn btn-outline-danger btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=config_file&action=edit" />Edit</a> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "config file" --> |  | ||||||
|  | @ -1,32 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "agents" --> |  | ||||||
|                 <div class="card text-center w-50 mx-auto"> |  | ||||||
|                     <p class="h4 card-header">Jilo Agent configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong></p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <p class="card-text">delete an agent:</p> |  | ||||||
|                         <form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config"> |  | ||||||
| <?php |  | ||||||
| foreach ($agentDetails[0] as $key => $value) { |  | ||||||
| //    if ($key === 'id') continue;
 |  | ||||||
| ?>
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="<?= htmlspecialchars($key) ?>" class="form-label"><?= htmlspecialchars($key) ?>:</label>
 |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <div class="text-start"><?= htmlspecialchars($value ?? '') ?></div>
 |  | ||||||
|                                     <input type="hidden" name="<?= htmlspecialchars($key) ?>" value="<?= htmlspecialchars($value ?? '') ?>" /> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| <?php } ?>
 |  | ||||||
|                             <br /> |  | ||||||
|                             <input type="hidden" name="agent" value="<?= htmlspecialchars($agentDetails[0]['id']) ?>" /> |  | ||||||
|                             <input type="hidden" name="delete" value="true" /> |  | ||||||
|                             <p class="h5 text-danger">Are you sure you want to delete this agent?</p> |  | ||||||
|                             <br /> |  | ||||||
|                             <a class="btn btn-secondary" href="<?= htmlspecialchars($app_root) ?>?page=config#platform<?= htmlspecialchars($platform_id) ?>agent<?= htmlspecialchars($agentDetails[0]['id']) ?>" />Cancel</a> |  | ||||||
|                             <input type="submit" class="btn btn-danger" value="Delete" /> |  | ||||||
|                         </form> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "agents" --> |  | ||||||
|  | @ -1,68 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- agents --> |  | ||||||
|                 <div class="card text-center w-50 mx-auto"> |  | ||||||
|                     <p class="h4 card-header">Jilo Agent configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong></p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <p class="card-text">edit the agent details:</p> |  | ||||||
|                         <form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config"> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="type_id" class="form-label">type</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <select class="form-control" type="text" name="type" id="agent_type_id" required> |  | ||||||
|                                         <option></option> |  | ||||||
| <?php foreach ($jilo_agent_types as $agent_type) { ?>
 |  | ||||||
|                                         <option value="<?= htmlspecialchars($agent_type['id']) ?>" <?php if ($agentDetails[0]['agent_type_id'] === $agent_type['id']) echo 'selected'; ?>>
 |  | ||||||
|                                             <?= htmlspecialchars($agent_type['description']) ?>
 |  | ||||||
|                                         </option> |  | ||||||
| <?php } ?>
 |  | ||||||
|                                     </select> |  | ||||||
|                                     <p class="text-start"><small>type of agent (meet, jvb, jibri, all)</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="url" class="form-label">URL</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="url" value="<?= htmlspecialchars($agentDetails[0]['url']) ?>" required /> |  | ||||||
|                                     <p class="text-start"><small>URL of the Jilo Agent API (https://example.com:8081)</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="secret_key" class="form-label">secret key</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="secret_key" value="<?= htmlspecialchars($agentDetails[0]['secret_key']) ?>" required /> |  | ||||||
|                                     <p class="text-start"><small>secret key for generating the access JWT token</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="check_period" class="form-label">check period</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="check_period" value="<?= htmlspecialchars($agentDetails[0]['check_period']) ?>" required /> |  | ||||||
|                                     <p class="text-start"><small>period in minutes for the automatic agent check (0 disables it)</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                             <br /> |  | ||||||
|                             <input type="hidden" name="agent" value="<?= htmlspecialchars($agentDetails[0]['id']) ?>" /> |  | ||||||
|                             <a class="btn btn-secondary" href="<?= htmlspecialchars($app_root) ?>?page=config#platform<?= htmlspecialchars($platform_id) ?>agent<?= htmlspecialchars($agentDetails[0]['id']) ?>" />Cancel</a> |  | ||||||
|                             <input type="submit" class="btn btn-primary" value="Save" /> |  | ||||||
|                         </form> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /agents --> |  | ||||||
|  | @ -1,50 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "hosts" --> |  | ||||||
|                 <div class="card text-center w-50 mx-lef"> |  | ||||||
|                     <p class="h4 card-header">Add new host in Jitsi platform <strong><?= htmlspecialchars($platformDetails[0]['name']) ?></strong></p>
 |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=config&item=host"> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="address" class="form-label">address</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="address" value="" required autofocus /> |  | ||||||
|                                     <p class="text-start"><small>DNS name or IP address of the machine</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="port" class="form-label">port</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="port" value="" required /> |  | ||||||
|                                     <p class="text-start"><small>port on which the Jilo Agent is listening</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="name" class="form-label">name</label> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="name" value="" /> |  | ||||||
|                                     <p class="text-start"><small>description or name of the host (optional)</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
|                             <input type="hidden" name="platform" value="<?= htmlspecialchars($platformDetails[0]['id'])?>" /> |  | ||||||
|                             <input type="hidden" name="item" value="host" /> |  | ||||||
|                             <input type="hidden" name="new" value="true" /> |  | ||||||
| 
 |  | ||||||
|                             <br /> |  | ||||||
|                             <a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_id) ?>&host=<?= htmlspecialchars($host) ?>#platform<?= htmlspecialchars($platform_id) ?>host<?= htmlspecialchars($host) ?>" />Cancel</a> |  | ||||||
|                                |  | ||||||
|                             <input type="submit" class="btn btn-primary btn-sm" value="Save" /> |  | ||||||
|                         </form> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "hosts" --> |  | ||||||
|  | @ -1,32 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "hosts" --> |  | ||||||
|                 <div class="card text-center w-50 mx-lef"> |  | ||||||
|                     <p class="h4 card-header">Jilo configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong></p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <p class="card-text">delete a host:</p> |  | ||||||
|                         <form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=config&item=host"> |  | ||||||
| <?php |  | ||||||
| foreach ($hostDetails[0] as $key => $value) { |  | ||||||
| ?>
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="<?= htmlspecialchars($key) ?>" class="form-label"><?= htmlspecialchars($key) ?>:</label>
 |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <div class="text-start"><?= htmlspecialchars($value ?? '') ?></div>
 |  | ||||||
|                                     <input type="hidden" name="<?= htmlspecialchars($key) ?>" value="<?= htmlspecialchars($value ?? '') ?>" /> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| <?php } ?>
 |  | ||||||
|                             <br /> |  | ||||||
|                             <input type="hidden" name="host" value="<?= htmlspecialchars($hostDetails[0]['id']) ?>" /> |  | ||||||
|                             <input type="hidden" name="delete" value="true" /> |  | ||||||
|                             <p class="h5 text-danger">Are you sure you want to delete this host?</p> |  | ||||||
|                             <br /> |  | ||||||
|                             <a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_id) ?>&host=<?= htmlspecialchars($host) ?>#platform<?= htmlspecialchars($platform_id) ?>host<?= htmlspecialchars($host) ?>" />Cancel</a> |  | ||||||
|                                |  | ||||||
|                             <input type="submit" class="btn btn-danger btn-sm" value="Delete" /> |  | ||||||
|                         </form> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "hosts" --> |  | ||||||
|  | @ -1,52 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "hosts" --> |  | ||||||
|                 <div class="card text-center w-50 mx-lef"> |  | ||||||
|                     <p class="h4 card-header">Jilo configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong></p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <p class="card-text">edit host details:</p> |  | ||||||
|                         <form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=config&item=host"> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="address" class="form-label">address</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="address" value="<?= htmlspecialchars($hostDetails[0]['address'] ?? '') ?>" required autofocus /> |  | ||||||
|                                     <p class="text-start"><small>DNS name or IP address of the machine</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="port" class="form-label">port</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="port" value="<?= htmlspecialchars($hostDetails[0]['port'] ?? '') ?>" required /> |  | ||||||
|                                     <p class="text-start"><small>port on which the Jilo Agent is listening</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="name" class="form-label">name</label> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="name" value="<?= htmlspecialchars($hostDetails[0]['name'] ?? '') ?>" /> |  | ||||||
|                                     <p class="text-start"><small>description or name of the host (optional)</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <input type="hidden" name="platform" value="<?= htmlspecialchars($platform_id) ?>" /> |  | ||||||
|                             <input type="hidden" name="item" value="host" /> |  | ||||||
|                             <input type="hidden" name="host" value="<?= htmlspecialchars($hostDetails[0]['id']) ?>" /> |  | ||||||
| 
 |  | ||||||
|                             <br /> |  | ||||||
|                             <a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_id) ?>&host=<?= htmlspecialchars($host) ?>#platform<?= htmlspecialchars($platform_id) ?>host<?= htmlspecialchars($host) ?>" />Cancel</a> |  | ||||||
|                                |  | ||||||
|                             <input type="submit" class="btn btn-primary btn-sm" value="Save" /> |  | ||||||
|                         </form> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "hosts" --> |  | ||||||
|  | @ -1,39 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "hosts" --> |  | ||||||
|                 <div class="card text-center w-75 mx-lef"> |  | ||||||
|                     <p class="h4 card-header">Jilo configuration :: Jitsi Meet hosts</p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <p class="card-text">Jitsi hosts configuration |  | ||||||
|                         </p> |  | ||||||
| <?php foreach ($platformsAll as $platform_array) { |  | ||||||
|     $hosts = $hostObject->getHostDetails($platform_array['id']); |  | ||||||
| ?>
 |  | ||||||
|                         <a name="platform<?= htmlspecialchars($platform_array['id']) ?>"></a> |  | ||||||
|                         <div class="row mb-1 border <?= isset($_REQUEST['platform']) && (int)$platform_array['id'] === (int)$_REQUEST['platform'] ? 'rounded bg-light' : '' ?>" style="padding: 20px; padding-bottom: 0px;"> |  | ||||||
|                             <p class="text-start"> |  | ||||||
|                                 platform <strong><?= htmlspecialchars($platform_array['name']) ?></strong>
 |  | ||||||
|                             </p> |  | ||||||
|                             <ul class="text-start" style="padding-left: 50px;"> |  | ||||||
| <?php foreach ($hosts as $host_array) { ?>
 |  | ||||||
|                                 <li style="padding-bottom: 10px;"> |  | ||||||
|                                     <a name="platform<?= htmlspecialchars($platform_array['id']) ?>host<?= htmlspecialchars($host_array['id']) ?>"></a> |  | ||||||
|                                     <span class="<?= isset($_REQUEST['platform']) && (int)$platform_array['id'] === (int)$_REQUEST['platform'] && isset($_REQUEST['host']) && (int)$host_array['id'] === (int)$_REQUEST['host'] ? 'border rounded bg-light' : '' ?>" style="padding: 10px;"> |  | ||||||
|                                         <?= htmlspecialchars($host_array['address']) ?>:<?= htmlspecialchars($host_array['port']) ?>
 |  | ||||||
|                                           |  | ||||||
|                                         <a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($host_array['platform_id']) ?>&host=<?= htmlspecialchars($host_array['id']) ?>&action=edit">edit host</a> |  | ||||||
|                                         <a class="btn btn-outline-danger btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($host_array['platform_id']) ?>&host=<?= htmlspecialchars($host_array['id']) ?>&action=delete">delete host</a> |  | ||||||
|                                     </span> |  | ||||||
|                                 </li> |  | ||||||
| <?php } ?>
 |  | ||||||
|                             </ul> |  | ||||||
|                             <p class="text-start" style="padding-left: 50px;"> |  | ||||||
|                                 total <?= htmlspecialchars(count($hosts)) ?> jilo <?= htmlspecialchars(count($hosts)) === '1' ? 'host' : 'hosts' ?> 
 |  | ||||||
|                                   |  | ||||||
|                                 <a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_array['id']) ?>&action=add">add new</a> |  | ||||||
|                             </p> |  | ||||||
| 
 |  | ||||||
|                         </div> |  | ||||||
| <?php } ?>
 |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "hosts" --> |  | ||||||
|  | @ -1,134 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "config" --> |  | ||||||
|                 <div class="card text-center w-75 mx-lef"> |  | ||||||
|                     <p class="h4 card-header">Jilo configuration</p> |  | ||||||
|                     <p class="h6 card-header"> |  | ||||||
|                         <span class="btn btn-outline-primary btn-sm active" aria-pressed="true" style="cursor: default;">platforms</span> |  | ||||||
|                         <a href="" class="btn btn-outline-primary btn-sm">hosts</a> |  | ||||||
|                         <a href="" class="btn btn-outline-primary btn-sm">endpoints</a> |  | ||||||
|                            |  | ||||||
|                         <a href="" class="btn btn-outline-primary btn-sm">config file</a> |  | ||||||
|                     </p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <p class="card-text">main variables</p> |  | ||||||
| <?php |  | ||||||
| include '../app/helpers/render.php'; |  | ||||||
| renderConfig($config, '0'); |  | ||||||
| echo "\n"; |  | ||||||
| ?>
 |  | ||||||
| 
 |  | ||||||
| <hr /> |  | ||||||
|                         <p class="card-text">platforms configuration  <a class="btn btn-secondary" style="padding: 0px;" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform&action=add">add new</a></p> |  | ||||||
| 
 |  | ||||||
| <?php foreach ($platformsAll as $platform_array) { |  | ||||||
|     $agents = $agentObject->getAgentDetails($platform_array['id']); |  | ||||||
| ?>
 |  | ||||||
| 
 |  | ||||||
|                         <a name="platform<?= htmlspecialchars($platform_array['id']) ?>"></a> |  | ||||||
|                         <div class="row mb-3" style="padding-left: 0px;"> |  | ||||||
|                             <div class="border bg-light" style="padding-left: 50px; padding-bottom: 0px; padding-top: 0px;"> |  | ||||||
|                                 <a style="text-decoration: none;" data-toggle="collapse" href="#collapsePlatform<?= htmlspecialchars($platform_array['id']) ?>" role="button" aria-expanded="true" aria-controls="collapsePlatform<?= htmlspecialchars($platform_array['id']) ?>"> |  | ||||||
|                                     <div class="border bg-white text-start mb-3 rounded mt-3" data-toggle="tooltip" data-placement="bottom" title="configuration for platform <?= htmlspecialchars($platform_array['id']) ?>"> |  | ||||||
|                                         <i class="fas fa-wrench"></i> |  | ||||||
|                                         <small>platform <?= htmlspecialchars($platform_array['id']) ?> (<?= htmlspecialchars($platform_array['name']) ?>)</small>
 |  | ||||||
|                                     </div> |  | ||||||
|                                 </a> |  | ||||||
|                                 <div class="collapse show" id="collapsePlatform<?= htmlspecialchars($platform_array['id']) ?>"> |  | ||||||
| 
 |  | ||||||
|                                     <div class="row mb-1" style="padding-left: 0px;"> |  | ||||||
|                                         <div class="col-md-8 text-start"> |  | ||||||
| 
 |  | ||||||
|                                             <div class="row mb-1"> |  | ||||||
|                                                 <div class="col-md-8 text-start"> |  | ||||||
|                                                     <a class="btn btn-secondary" style="padding: 2px;" href="<?= htmlspecialchars($app_root) ?>?page=config&platform=<?= htmlspecialchars($platform_array['id']) ?>&action=edit">edit platform</a> |  | ||||||
| <?php if (count($platformsAll) <= 1) { ?>
 |  | ||||||
|                                                     <span class="btn btn-light" style="padding: 2px;" href="#" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="can't delete the last platform">delete platform</span> |  | ||||||
| <?php } else { ?>
 |  | ||||||
|                                                     <a class="btn btn-danger" style="padding: 2px;" href="<?= htmlspecialchars($app_root) ?>?page=config&platform=<?= htmlspecialchars($platform_array['id']) ?>&action=delete">delete platform</a> |  | ||||||
| <?php } ?>
 |  | ||||||
|                                                 </div> |  | ||||||
|                                             </div> |  | ||||||
| 
 |  | ||||||
|                                         </div> |  | ||||||
|                                         <div style="padding-left: 100px; padding-bottom: 20px;"> |  | ||||||
| <?php foreach ($platform_array as $key => $value) { |  | ||||||
|         if ($key === 'id') continue; |  | ||||||
| ?>
 |  | ||||||
|                                             <div class="row mb-1" style="padding-left: 100px;"> |  | ||||||
|                                                 <div class="col-md-4 text-end"> |  | ||||||
|                                                     <?= htmlspecialchars($key) ?>:
 |  | ||||||
|                                                 </div> |  | ||||||
|                                                 <div class="border col-md-8 text-start"> |  | ||||||
|                                                     <?= htmlspecialchars($value) ?>
 |  | ||||||
|                                                 </div> |  | ||||||
|                                             </div> |  | ||||||
| <?php } ?>
 |  | ||||||
| 
 |  | ||||||
|                                         </div> |  | ||||||
|                                         <hr /> |  | ||||||
|                                         <p class="card-text">jilo agents on platform <?= htmlspecialchars($platform_array['id']) ?> (<?= htmlspecialchars($platform_array['name']) ?>)
 |  | ||||||
|                                             <br /> |  | ||||||
|                                             total <?= htmlspecialchars(count($agents)) ?> <?= htmlspecialchars(count($agents)) === 1 ? 'jilo agent' : 'jilo agents' ?> 
 |  | ||||||
|                                             <a class="btn btn-secondary" style="padding: 0px;" href="<?= htmlspecialchars($app_root) ?>?page=config&platform=<?= htmlspecialchars($platform_array['id']) ?>&action=add-agent"> |  | ||||||
|                                                 add new |  | ||||||
|                                             </a> |  | ||||||
|                                         </p> |  | ||||||
| 
 |  | ||||||
| <?php foreach ($agents as $agent_array) { ?>
 |  | ||||||
| 
 |  | ||||||
|                                         <a name="platform<?= htmlspecialchars($platform_array['id']) ?>agent<?= htmlspecialchars($agent_array['id']) ?>"></a> |  | ||||||
|                                         <div class="row mb-3" style="padding-left: 0px;"> |  | ||||||
|                                             <div class="border rounded bg-light" style="padding-left: 50px; padding-bottom: 20px; padding-top: 20px;"> |  | ||||||
|                                                 <div class="row mb-1" style="padding-left: 0px;"> |  | ||||||
|                                                     <div class="col-md-4 text-end"> |  | ||||||
|                                                         agent id <?= htmlspecialchars($agent_array['id']) ?>:
 |  | ||||||
|                                                     </div> |  | ||||||
|                                                     <div class="col-md-8 text-start"> |  | ||||||
|                                                         <a class="btn btn-secondary" style="padding: 2px;" href="<?= htmlspecialchars($app_root) ?>?page=config&platform=<?= htmlspecialchars($agent_array['platform_id']) ?>&agent=<?= htmlspecialchars($agent_array['id']) ?>&action=edit">edit agent</a> |  | ||||||
|                                                         <a class="btn btn-danger" style="padding: 2px;" href="<?= htmlspecialchars($app_root) ?>?page=config&platform=<?= htmlspecialchars($agent_array['platform_id']) ?>&agent=<?= htmlspecialchars($agent_array['id']) ?>&action=delete">delete agent</a> |  | ||||||
|                                                     </div> |  | ||||||
|                                                     <div style="padding-left: 100px; padding-bottom: 20px;"> |  | ||||||
|                                                         <div class="row mb-1" style="padding-left: 100px;"> |  | ||||||
|                                                             <div class="col-md-4 text-end"> |  | ||||||
|                                                                 agent type: |  | ||||||
|                                                             </div> |  | ||||||
|                                                             <div class="border col-md-8 text-start"> |  | ||||||
|                                                                 <?= htmlspecialchars($agent_array['agent_description']) ?>
 |  | ||||||
|                                                             </div> |  | ||||||
|                                                         </div> |  | ||||||
|                                                         <div class="row mb-1" style="padding-left: 100px;"> |  | ||||||
|                                                             <div class="col-md-4 text-end"> |  | ||||||
|                                                                 endpoint: |  | ||||||
|                                                             </div> |  | ||||||
|                                                             <div class="border col-md-8 text-start"> |  | ||||||
|                                                                 <?= htmlspecialchars($agent_array['url'].$agent_array['agent_endpoint']) ?>
 |  | ||||||
|                                                             </div> |  | ||||||
|                                                         </div> |  | ||||||
| <?php if (isset($agent_array['check_period']) && $agent_array['check_period'] !== 0) { ?>
 |  | ||||||
|                                                         <div class="row mb-1" style="padding-left: 100px;"> |  | ||||||
|                                                             <div class="col-md-4 text-end"> |  | ||||||
|                                                                 check period: |  | ||||||
|                                                             </div> |  | ||||||
|                                                             <div class="border col-md-8 text-start"> |  | ||||||
|                                                                 <?= htmlspecialchars($agent_array['check_period']) ?> <?= ($agent_array['check_period'] == 1 ? 'minute' : 'minutes') ?>
 |  | ||||||
|                                                             </div> |  | ||||||
|                                                         </div> |  | ||||||
| <?php } ?>
 |  | ||||||
|                                                     </div> |  | ||||||
|                                                 </div> |  | ||||||
|                                             </div> |  | ||||||
|                                         </div> |  | ||||||
| <?php } ?>
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                                     </div> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
| <?php } ?>
 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "config" --> |  | ||||||
|  | @ -1,51 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "platforms" --> |  | ||||||
|                 <div class="card text-center w-50 mx-lef"> |  | ||||||
|                     <p class="h4 card-header">Add new Jitsi platform</p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <!--p class="card-text">add new platform:</p--> |  | ||||||
|                         <form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config&item=platform"> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="name" class="form-label">name</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="name" value="" required autofocus /> |  | ||||||
|                                     <p class="text-start"><small>descriptive name for the platform</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="jitsi_url" class="form-label">Jitsi URL</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="jitsi_url" value="https://" required /> |  | ||||||
|                                     <p class="text-start"><small>URL of the Jitsi Meet (used for checks and for loading config.js)</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="jilo_database" class="form-label">jilo_database</label> |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="jilo_database" value="" required /> |  | ||||||
|                                     <p class="text-start"><small>path to the database file (relative to the app root)</small></p> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| 
 |  | ||||||
|                             <input type="hidden" name="new" value="true" /> |  | ||||||
| 
 |  | ||||||
|                             <br /> |  | ||||||
|                             <a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform" />Cancel</a> |  | ||||||
|                                |  | ||||||
|                             <input type="submit" class="btn btn-primary btn-sm" value="Save" /> |  | ||||||
|                         </form> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "platforms" --> |  | ||||||
|  | @ -1,32 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "platforms" --> |  | ||||||
|                 <div class="card text-center w-50 mx-lef"> |  | ||||||
|                     <p class="h4 card-header">Jilo configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong> :: delete</p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config"> |  | ||||||
| <?php |  | ||||||
| foreach ($platformDetails[0] as $key => $value) { |  | ||||||
|     if ($key === 'id') continue; |  | ||||||
| ?>
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="<?= htmlspecialchars($key) ?>" class="form-label"><?= htmlspecialchars($key) ?>:</label>
 |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <div class="text-start"><?= htmlspecialchars($value) ?? '' ?></div>
 |  | ||||||
|                                     <input type="hidden" name="<?= htmlspecialchars($key) ?>" value="<?= htmlspecialchars($value ?? '')?>" /> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| <?php } ?>
 |  | ||||||
|                             <br /> |  | ||||||
|                             <input type="hidden" name="platform" value="<?= htmlspecialchars($platform_id) ?>" /> |  | ||||||
|                             <input type="hidden" name="delete" value="true" /> |  | ||||||
|                             <p class="h5 text-danger">Are you sure you want to delete this platform?</p> |  | ||||||
|                             <br /> |  | ||||||
|                             <a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform&platform=<?= htmlspecialchars($platform_id) ?>#platform<?= htmlspecialchars($platform_id) ?>" />Cancel</a> |  | ||||||
|                                |  | ||||||
|                             <input type="submit" class="btn btn-danger btn-sm" value="Delete" /> |  | ||||||
|                         </form> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "platforms" --> |  | ||||||
|  | @ -1,36 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "platforms" --> |  | ||||||
|                 <div class="card text-center w-50 mx-lef"> |  | ||||||
|                     <p class="h4 card-header">Jilo configuration for Jitsi platform <strong>"<?= htmlspecialchars($platformDetails[0]['name']) ?>"</strong> :: edit</p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <form method="POST" action="<?= htmlspecialchars($app_root) ?>?platform=<?= htmlspecialchars($platform_id) ?>&page=config&item=platform"> |  | ||||||
| <?php |  | ||||||
| foreach ($platformDetails[0] as $key => $value) { |  | ||||||
|     if ($key === 'id') continue; |  | ||||||
| ?>
 |  | ||||||
|                             <div class="row mb-3"> |  | ||||||
|                                 <div class="col-md-4 text-end"> |  | ||||||
|                                     <label for="<?= htmlspecialchars($config_item) ?>" class="form-label"><?= htmlspecialchars($key) ?></label>
 |  | ||||||
|                                     <span class="text-danger" style="margin-right: -12px;">*</span> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="col-md-8"> |  | ||||||
|                                     <input class="form-control" type="text" name="<?= htmlspecialchars($key) ?>" value="<?= htmlspecialchars($value ?? '') ?>" required autofocus /> |  | ||||||
| <?php if ($key === 'name') { ?>
 |  | ||||||
|                                     <p class="text-start"><small>descriptive name for the platform</small></p> |  | ||||||
| <?php } elseif ($key === 'jitsi_url') { ?>
 |  | ||||||
|                                     <p class="text-start"><small>URL of the Jitsi Meet (used for checks and for loading config.js)</small></p> |  | ||||||
| <?php } elseif ($key === 'jilo_database') { ?>
 |  | ||||||
|                                     <p class="text-start"><small>path to the database file (relative to the app root)</small></p> |  | ||||||
| <?php } ?>
 |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
| <?php } ?>
 |  | ||||||
|                             <br /> |  | ||||||
|                             <input type="hidden" name="platform" value="<?= htmlspecialchars($platform_id) ?>" /> |  | ||||||
|                             <a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform&platform=<?= htmlspecialchars($platform_id) ?>#platform<?= htmlspecialchars($platform_id) ?>" />Cancel</a> |  | ||||||
|                                |  | ||||||
|                             <input type="submit" class="btn btn-primary btn-sm" value="Save" /> |  | ||||||
|                         </form> |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "platforms" --> |  | ||||||
|  | @ -1,60 +0,0 @@ | ||||||
| 
 |  | ||||||
|                 <!-- widget "platforms" --> |  | ||||||
|                 <div class="card text-center w-75 mx-lef"> |  | ||||||
|                     <p class="h4 card-header">Jilo configuration :: Jitsi Meet platforms</p> |  | ||||||
|                     <div class="card-body"> |  | ||||||
|                         <p class="card-text">Jitsi platforms configuration  <a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform&action=add">add new</a></p> |  | ||||||
| <?php foreach ($platformsAll as $platform_array) { |  | ||||||
|     $hosts = $hostObject->getHostDetails($platform_array['id']); |  | ||||||
|     $agents = $agentObject->getAgentDetails($platform_array['id']); |  | ||||||
| ?>
 |  | ||||||
|                         <a name="platform<?= htmlspecialchars($platform_array['id']) ?>"></a> |  | ||||||
|                         <div class="row mb-1 border<?= isset($_REQUEST['platform']) && (int)$platform_array['id'] === (int)$_REQUEST['platform'] ? ' bg-light' : '' ?>" style="padding: 20px; padding-bottom: 0px;"> |  | ||||||
|                             <p> |  | ||||||
|                                 platform id <?= htmlspecialchars($platform_array['id']) ?> - <strong><?= htmlspecialchars($platform_array['name']) ?></strong>
 |  | ||||||
|                                   |  | ||||||
|                                 <a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform&platform=<?= htmlspecialchars($platform_array['id']) ?>&action=edit">edit platform</a> |  | ||||||
| <?php if (count($platformsAll) <= 1) { ?>
 |  | ||||||
|                                 <span class="btn btn-outline-light btn-sm" href="#" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="can't delete the last platform">delete platform</span> |  | ||||||
| <?php } else { ?>
 |  | ||||||
|                                 <a class="btn btn-outline-danger btn-sm" href="<?= htmlspecialchars($app_root) ?>?page=config&item=platform&platform=<?= htmlspecialchars($platform_array['id']) ?>&action=delete">delete platform</a> |  | ||||||
| <?php } ?>
 |  | ||||||
|                             </p> |  | ||||||
|                             <div style="padding-left: 100px; padding-bottom: 20px;"> |  | ||||||
| <?php foreach ($platform_array as $key => $value) { |  | ||||||
|         if ($key === 'id') continue; |  | ||||||
| ?>
 |  | ||||||
|                                 <div class="row mb-1" style="padding-left: 100px;"> |  | ||||||
|                                     <div class="col-md-4 text-end"> |  | ||||||
|                                         <?= htmlspecialchars($key) ?>:
 |  | ||||||
|                                     </div> |  | ||||||
|                                     <div class="col-md-8 text-start"> |  | ||||||
| <?php if ($key === 'jitsi_url') { ?>
 |  | ||||||
|                                         <a href="<?= htmlspecialchars($value) ?>" target="_blank" rel="noopener noreferrer" data-toggle="tooltip" data-placement="right" data-offset="30.0" title="open the Jitsi Meet platform in a new window"> |  | ||||||
|                                             <?= htmlspecialchars($value) ?>
 |  | ||||||
|                                             <i class="fas fa-external-link-alt"></i> |  | ||||||
|                                         </a> |  | ||||||
| <?php } else { ?>
 |  | ||||||
|                                         <?= htmlspecialchars($value) ?>
 |  | ||||||
| <?php } ?>
 |  | ||||||
|                                     </div> |  | ||||||
|                                 </div> |  | ||||||
| <?php } ?>
 |  | ||||||
|                                 <div class="row mb-1" style="padding-left: 100px;"> |  | ||||||
|                                     <div class="col-md-4 text-end"></div> |  | ||||||
|                                     <div class="col-md-8 text-start"> |  | ||||||
|                                         <a href="<?= htmlspecialchars($app_root) ?>?page=config&item=host&platform=<?= htmlspecialchars($platform_array['id']) ?>#platform<?= htmlspecialchars($platform_array['id']) ?>"><?= htmlspecialchars(count($hosts)) ?> <?= htmlspecialchars(count($hosts)) === '1' ? 'host' : 'hosts' ?></a>
 |  | ||||||
|                                     </div> |  | ||||||
|                                 </div> |  | ||||||
|                                 <div class="row mb-1" style="padding-left: 100px;"> |  | ||||||
|                                     <div class="col-md-4 text-end"></div> |  | ||||||
|                                     <div class="col-md-8 text-start"> |  | ||||||
|                                         <a href="<?= htmlspecialchars($app_root) ?>?page=config&item=endpoint&platform=<?= htmlspecialchars($platform_array['id']) ?>#platform<?= htmlspecialchars($platform_array['id']) ?>"><?= htmlspecialchars(count($agents)) ?> <?= htmlspecialchars(count($agents)) === '1' ? 'endpoint' : 'endpoints' ?></a>
 |  | ||||||
|                                     </div> |  | ||||||
|                                 </div> |  | ||||||
|                             </div> |  | ||||||
|                         </div> |  | ||||||
| <?php } ?>
 |  | ||||||
|                     </div> |  | ||||||
|                 </div> |  | ||||||
|                 <!-- /widget "platforms" --> |  | ||||||
|  | @ -0,0 +1,198 @@ | ||||||
|  | 
 | ||||||
|  | <!-- config file --> | ||||||
|  | <div class="container-fluid mt-2"> | ||||||
|  |     <div class="row mb-4"> | ||||||
|  |         <div class="col-12 mb-4"> | ||||||
|  |             <h2>Configuration</h2> | ||||||
|  |             <small><?= htmlspecialchars($config['site_name']) ?> configuration file: <em><?= htmlspecialchars($localConfigPath) ?></em></small>
 | ||||||
|  | <?php if ($configMessage) { ?>
 | ||||||
|  |             <?= $configMessage ?>
 | ||||||
|  | <?php } ?>
 | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div class="card shadow-sm"> | ||||||
|  |         <div class="card-header bg-light d-flex justify-content-between align-items-center py-3"> | ||||||
|  |             <h5 class="card-title mb-0"> | ||||||
|  |                 <i class="fas fa-wrench me-2 text-secondary"></i> | ||||||
|  |                 <?= htmlspecialchars($config['site_name']) ?> app configuration
 | ||||||
|  |             </h5> | ||||||
|  | <?php if ($userObject->hasRight($userId, 'superuser') || | ||||||
|  |           $userObject->hasRight($userId, 'edit config file')) { ?>
 | ||||||
|  |             <div> | ||||||
|  |                 <button type="button" class="btn btn-outline-primary btn-sm toggle-edit" <?= !$isWritable ? 'disabled' : '' ?>>
 | ||||||
|  |                     <i class="fas fa-edit me-2"></i>Edit | ||||||
|  |                 </button> | ||||||
|  |                 <div class="edit-controls d-none"> | ||||||
|  |                     <button type="button" class="btn btn-danger btn-sm save-config"> | ||||||
|  |                         <i class="fas fa-save me-2"></i>Save | ||||||
|  |                     </button> | ||||||
|  |                     <button type="button" class="btn btn-outline-secondary btn-sm ms-2 cancel-edit"> | ||||||
|  |                         Cancel | ||||||
|  |                     </button> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  | <?php } ?>
 | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <div class="card-body p-4"> | ||||||
|  |             <form id="configForm"> | ||||||
|  | <?php | ||||||
|  | include CSRF_TOKEN_INCLUDE; | ||||||
|  | 
 | ||||||
|  | function renderConfigItem($key, $value, $path = '') { | ||||||
|  |     $fullPath = $path ? $path . '[' . $key . ']' : $key; | ||||||
|  |     // Only capitalize first letter, not every word
 | ||||||
|  |     $displayName = ucfirst(str_replace('_', ' ', $key)); | ||||||
|  | 
 | ||||||
|  |     if (is_array($value)) { | ||||||
|  |         echo "\t\t\t\t\t\t\t\t<div class=\"config-section mb-4\">"; | ||||||
|  |         echo "\n\t\t\t\t\t\t\t\t\t<h6 class=\"border-bottom pb-2 mb-3\">" . htmlspecialchars($displayName) . '</h6>'; | ||||||
|  |         echo "\n\t\t\t\t\t\t\t\t\t<div class=\"ps-4\">\n"; | ||||||
|  |         foreach ($value as $subKey => $subValue) { | ||||||
|  |             renderConfigItem($subKey, $subValue, $fullPath); | ||||||
|  |         } | ||||||
|  |         echo "\t\t\t\t\t\t\t\t\t</div>\n"; | ||||||
|  |         echo "\t\t\t\t\t\t\t\t</div>\n"; | ||||||
|  |     } else { | ||||||
|  | ?>
 | ||||||
|  |                 <div class="config-item row mb-3 align-items-center"> | ||||||
|  |                     <div class="col-md-4 text-end"> | ||||||
|  |                         <label class="form-label mb-0"><?= htmlspecialchars($displayName) ?></label>
 | ||||||
|  |                     </div> | ||||||
|  |                     <div class="col-md-8"> | ||||||
|  |                         <div class="view-mode"> | ||||||
|  | <?php if (is_bool($value) || $key === 'registration_enabled') { ?>
 | ||||||
|  |                             <span class="badge <?= $value ? 'bg-success' : 'bg-secondary' ?>"><?= $value ? 'Enabled' : 'Disabled' ?></span>
 | ||||||
|  | <?php } elseif ($key === 'environment') { ?>
 | ||||||
|  |                             <span class="badge <?= $value === 'production' ? 'bg-danger' : 'bg-info' ?>"><?= htmlspecialchars($value) ?></span>
 | ||||||
|  | <?php } else { | ||||||
|  |         if (empty($value) && $value !== '0') { ?>
 | ||||||
|  |                             <span class="text-muted fst-italic">blank</span> | ||||||
|  | <?php   } else { ?>
 | ||||||
|  |                             <span class="text-body"><?= htmlspecialchars($value) ?></span>
 | ||||||
|  | <?php   } ?>
 | ||||||
|  | <?php } ?>
 | ||||||
|  |                         </div> | ||||||
|  |                         <div class="edit-mode d-none"> | ||||||
|  | <?php if (is_bool($value) || $key === 'registration_enabled') { ?>
 | ||||||
|  |                             <div class="form-check form-switch"> | ||||||
|  |                                 <input class="form-check-input" type="checkbox" name="<?= htmlspecialchars($fullPath) ?>" <?= $value ? 'checked' : '' ?>>
 | ||||||
|  |                             </div> | ||||||
|  | <?php } elseif ($key === 'environment') { ?>
 | ||||||
|  |                             <select class="form-select form-select-sm" name="<?= htmlspecialchars($fullPath) ?>"> | ||||||
|  |                                 <option value="development" <?= $value === 'development' ? 'selected' : '' ?>>development</option>
 | ||||||
|  |                                 <option value="production" <?= $value === 'production' ? 'selected' : '' ?>>production</option>
 | ||||||
|  |                             </select> | ||||||
|  | <?php } else { ?>
 | ||||||
|  |                             <input type="text" class="form-control form-control-sm" name="<?= htmlspecialchars($fullPath) ?>" value="<?= htmlspecialchars($value) ?>"> | ||||||
|  | <?php } ?>
 | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  | <?php | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  |     foreach ($config as $key => $value) { | ||||||
|  |         renderConfigItem($key, $value); | ||||||
|  |     } ?>
 | ||||||
|  |             </form> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | $(function() { | ||||||
|  |     // Toggle edit mode
 | ||||||
|  |     $('.toggle-edit').click(function() { | ||||||
|  |         $(this).hide(); | ||||||
|  |         $('.edit-controls').removeClass('d-none'); | ||||||
|  |         $('.view-mode').hide(); | ||||||
|  |         $('.edit-mode').removeClass('d-none'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Cancel edit
 | ||||||
|  |     $('.cancel-edit').click(function() { | ||||||
|  |         $('.toggle-edit').show(); | ||||||
|  |         $('.edit-controls').addClass('d-none'); | ||||||
|  |         $('.view-mode').show(); | ||||||
|  |         $('.edit-mode').addClass('d-none'); | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     // Save config
 | ||||||
|  |     $('.save-config').click(function() { | ||||||
|  |         const $btn = $(this).prop('disabled', true).html('<i class="fas fa-spinner fa-spin me-2"></i>Saving...'); | ||||||
|  | 
 | ||||||
|  |         // Build form data object
 | ||||||
|  |         const data = {}; | ||||||
|  | 
 | ||||||
|  |         // Handle text inputs
 | ||||||
|  |         $('#configForm input[type="text"]').each(function() { | ||||||
|  |             data[$(this).attr('name')] = $(this).val(); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // Handle checkboxes
 | ||||||
|  |         $('#configForm input[type="checkbox"]').each(function() { | ||||||
|  |             data[$(this).attr('name')] = $(this).prop('checked') ? '1' : '0'; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // Handle selects
 | ||||||
|  |         $('#configForm select').each(function() { | ||||||
|  |             data[$(this).attr('name')] = $(this).val(); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         $.ajax({ | ||||||
|  |             url: '<?= htmlspecialchars($app_root) ?>?page=config', | ||||||
|  |             method: 'POST', | ||||||
|  |             data: JSON.stringify(data), | ||||||
|  |             contentType: 'application/json', | ||||||
|  |             headers: { | ||||||
|  |                 'X-Requested-With': 'XMLHttpRequest', | ||||||
|  |                 'X-CSRF-Token': $('input[name="csrf_token"]').val() | ||||||
|  |             }, | ||||||
|  |             success: function(response) { | ||||||
|  |                 if (response.success) { | ||||||
|  |                     JsMessages.success(response.message || 'Config file updated successfully'); | ||||||
|  | 
 | ||||||
|  |                     $('#configForm input[type="text"], #configForm input[type="checkbox"], #configForm select').each(function() { | ||||||
|  |                         const $input = $(this); | ||||||
|  |                         const $item = $input.closest('.config-item'); | ||||||
|  |                         const $viewMode = $item.find('.view-mode'); | ||||||
|  | 
 | ||||||
|  |                         if ($item.length) { | ||||||
|  |                             let value; | ||||||
|  |                             if ($input.is('[type="checkbox"]')) { | ||||||
|  |                                 value = $input.prop('checked') ? '1' : '0'; | ||||||
|  |                                 const isEnabled = value === '1'; | ||||||
|  |                                 $viewMode.html(`<span class="badge ${isEnabled ? 'bg-success' : 'bg-secondary'}">${isEnabled ? 'Enabled' : 'Disabled'}</span>`); | ||||||
|  |                             } else if ($input.is('select')) { | ||||||
|  |                                 value = $input.val(); | ||||||
|  |                                 $viewMode.html(`<span class="badge ${value === 'production' ? 'bg-danger' : 'bg-info'}">${value}</span>`); | ||||||
|  |                             } else { | ||||||
|  |                                 value = $input.val(); | ||||||
|  |                                 $viewMode.html(value === '' ? '<span class="text-muted fst-italic">blank</span>' : `<span class="text-body">${value}</span>`); | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                     // Finally, exit edit mode
 | ||||||
|  |                     $('.toggle-edit').show(); | ||||||
|  |                     $('.edit-controls').addClass('d-none'); | ||||||
|  |                     $('.view-mode').show(); | ||||||
|  |                     $('.edit-mode').addClass('d-none'); | ||||||
|  |                 } else { | ||||||
|  |                     JsMessages.error(response.error || 'Error saving config'); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             error: function(xhr, status, error) { | ||||||
|  |                 JsMessages.error('Error saving config: ' + error); | ||||||
|  |             }, | ||||||
|  |             complete: function() { | ||||||
|  |                 $btn.prop('disabled', false).html('<i class="fas fa-save me-2"></i>Save'); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | <!-- /config file --> | ||||||
|  | @ -0,0 +1,108 @@ | ||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * Two-factor authentication setup template | ||||||
|  |  */ | ||||||
|  | ?>
 | ||||||
|  | 
 | ||||||
|  | <div class="container mt-4"> | ||||||
|  |     <div class="row justify-content-center"> | ||||||
|  |         <div class="col-md-8"> | ||||||
|  |             <div class="card"> | ||||||
|  |                 <div class="card-header"> | ||||||
|  |                     <h3>Set up two-factor authentication</h3> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="card-body"> | ||||||
|  |                     <div class="alert alert-info"> | ||||||
|  |                         <p>Two-factor authentication adds an extra layer of security to your account. Once enabled, you'll need to enter both your password and a code from your authenticator app when signing in.</p> | ||||||
|  |                     </div> | ||||||
|  | 
 | ||||||
|  |                     <?php if (isset($error)): ?>
 | ||||||
|  |                         <div class="alert alert-danger"> | ||||||
|  |                             <?php echo htmlspecialchars($error); ?>
 | ||||||
|  |                         </div> | ||||||
|  |                     <?php endif; ?>
 | ||||||
|  | 
 | ||||||
|  |                     <?php if (isset($setupData) && is_array($setupData)): ?>
 | ||||||
|  |                         <div class="setup-steps"> | ||||||
|  |                             <h4>1. Install an authenticator app</h4> | ||||||
|  |                             <p>If you haven't already, install an authenticator app on your mobile device:</p> | ||||||
|  |                             <ul> | ||||||
|  |                                 <li>Google Authenticator</li> | ||||||
|  |                                 <li>Microsoft Authenticator</li> | ||||||
|  |                                 <li>Authy</li> | ||||||
|  |                             </ul> | ||||||
|  | 
 | ||||||
|  |                             <h4 class="mt-4">2. Scan the QR code</h4> | ||||||
|  |                             <p>Open your authenticator app and scan this QR code:</p> | ||||||
|  | 
 | ||||||
|  |                             <div class="text-center my-4"> | ||||||
|  |                                 <div id="qrcode"></div> | ||||||
|  |                                 <div class="mt-2"> | ||||||
|  |                                     <small class="text-muted">Can't scan? Use this code instead:</small><br> | ||||||
|  |                                     <code class="secret-key"><?php echo htmlspecialchars($setupData['secret']); ?></code>
 | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  | 
 | ||||||
|  |                             <h4 class="mt-4">3. Verify setup</h4> | ||||||
|  |                             <p>Enter the 6-digit code from your authenticator app to verify the setup:</p> | ||||||
|  | 
 | ||||||
|  |                             <form method="post" action="?page=credentials&item=2fa&action=setup" class="mt-3"> | ||||||
|  |                                 <div class="form-group"> | ||||||
|  |                                     <input type="text"  | ||||||
|  |                                            name="code"  | ||||||
|  |                                            class="form-control"  | ||||||
|  |                                            pattern="[0-9]{6}"  | ||||||
|  |                                            maxlength="6" | ||||||
|  |                                            required | ||||||
|  |                                            placeholder="Enter 6-digit code"> | ||||||
|  |                                 </div> | ||||||
|  | 
 | ||||||
|  |                                 <input type="hidden" name="secret" value="<?php echo htmlspecialchars($setupData['secret']); ?>"> | ||||||
|  |                                 <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>"> | ||||||
|  | 
 | ||||||
|  |                                 <button type="submit" class="btn btn-primary mt-3"> | ||||||
|  |                                     Verify and enable 2FA | ||||||
|  |                                 </button> | ||||||
|  |                             </form> | ||||||
|  | 
 | ||||||
|  |                             <div class="mt-4"> | ||||||
|  |                                 <h4>Backup codes</h4> | ||||||
|  |                                 <p class="text-warning"> | ||||||
|  |                                     <strong>Important:</strong> Save these backup codes in a secure place.  | ||||||
|  |                                     If you lose access to your authenticator app, you can use these codes to sign in. | ||||||
|  |                                     Each code can only be used once. | ||||||
|  |                                 </p> | ||||||
|  |                                 <div class="backup-codes bg-light p-3 rounded"> | ||||||
|  |                                     <?php foreach ($setupData['backupCodes'] as $code): ?>
 | ||||||
|  |                                         <code class="d-block"><?php echo htmlspecialchars($code); ?></code>
 | ||||||
|  |                                     <?php endforeach; ?>
 | ||||||
|  |                                 </div> | ||||||
|  |                                 <button class="btn btn-secondary mt-2" onclick="window.print()"> | ||||||
|  |                                     Print backup codes | ||||||
|  |                                 </button> | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                     <?php else: ?>
 | ||||||
|  |                         <div class="alert alert-danger"> | ||||||
|  |                             Failed to generate 2FA setup data. Please try again. | ||||||
|  |                         </div> | ||||||
|  |                         <a href="?page=credentials" class="btn btn-primary">Back to credentials</a> | ||||||
|  |                     <?php endif; ?>
 | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <?php if (isset($setupData) && is_array($setupData)): ?>
 | ||||||
|  | <script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script> | ||||||
|  | <script> | ||||||
|  | document.addEventListener('DOMContentLoaded', function() { | ||||||
|  |     new QRCode(document.getElementById("qrcode"), { | ||||||
|  |         text: <?php echo json_encode($setupData['otpauthUrl']); ?>,
 | ||||||
|  |         width: 200, | ||||||
|  |         height: 200 | ||||||
|  |     }); | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | <?php endif; ?>
 | ||||||
|  | @ -0,0 +1,86 @@ | ||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * Two-factor authentication verification template | ||||||
|  |  */ | ||||||
|  | ?>
 | ||||||
|  | 
 | ||||||
|  | <div class="container mt-4"> | ||||||
|  |     <div class="row justify-content-center"> | ||||||
|  |         <div class="col-md-6"> | ||||||
|  |             <div class="card"> | ||||||
|  |                 <div class="card-header"> | ||||||
|  |                     <h3>Two-factor authentication</h3> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="card-body"> | ||||||
|  |                     <?php if (isset($error)): ?>
 | ||||||
|  |                         <div class="alert alert-danger"> | ||||||
|  |                             <?php echo htmlspecialchars($error); ?>
 | ||||||
|  |                         </div> | ||||||
|  |                     <?php endif; ?>
 | ||||||
|  | 
 | ||||||
|  |                     <p>Enter the 6-digit code from your authenticator app:</p> | ||||||
|  | 
 | ||||||
|  |                     <form method="post" action="?page=login&action=verify" class="mt-3"> | ||||||
|  |                         <div class="form-group"> | ||||||
|  |                             <input type="text"  | ||||||
|  |                                    name="code"  | ||||||
|  |                                    class="form-control form-control-lg text-center"  | ||||||
|  |                                    pattern="[0-9]{6}"  | ||||||
|  |                                    maxlength="6" | ||||||
|  |                                    inputmode="numeric" | ||||||
|  |                                    autocomplete="one-time-code" | ||||||
|  |                                    required | ||||||
|  |                                    autofocus | ||||||
|  |                                    placeholder="000000"> | ||||||
|  |                         </div> | ||||||
|  | 
 | ||||||
|  |                         <input type="hidden" name="user_id" value="<?php echo htmlspecialchars($userId); ?>"> | ||||||
|  | 
 | ||||||
|  |                         <button type="submit" class="btn btn-primary btn-block mt-4"> | ||||||
|  |                             Verify code | ||||||
|  |                         </button> | ||||||
|  |                     </form> | ||||||
|  | 
 | ||||||
|  |                     <div class="mt-4"> | ||||||
|  |                         <p class="text-muted text-center"> | ||||||
|  |                             Lost access to your authenticator app?<br> | ||||||
|  |                             <a href="#" data-toggle="collapse" data-target="#backupCodeForm"> | ||||||
|  |                                 Use a backup code | ||||||
|  |                             </a> | ||||||
|  |                         </p> | ||||||
|  | 
 | ||||||
|  |                         <div class="collapse mt-3" id="backupCodeForm"> | ||||||
|  |                             <form method="post" action="?page=login&action=verify" class="mt-3"> | ||||||
|  |                                 <div class="form-group"> | ||||||
|  |                                     <label>Enter backup code:</label> | ||||||
|  |                                     <input type="text"  | ||||||
|  |                                            name="backup_code"  | ||||||
|  |                                            class="form-control" | ||||||
|  |                                            pattern="[a-f0-9]{8}" | ||||||
|  |                                            maxlength="8" | ||||||
|  |                                            required | ||||||
|  |                                            placeholder="Enter backup code"> | ||||||
|  |                                 </div> | ||||||
|  | 
 | ||||||
|  |                                 <input type="hidden" name="user_id" value="<?php echo htmlspecialchars($userId); ?>"> | ||||||
|  | 
 | ||||||
|  |                                 <button type="submit" class="btn btn-secondary btn-block"> | ||||||
|  |                                     Use backup code | ||||||
|  |                                 </button> | ||||||
|  |                             </form> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | // Auto-submit when 6 digits are entered
 | ||||||
|  | document.querySelector('input[name="code"]').addEventListener('input', function(e) { | ||||||
|  |     if (e.target.value.length === 6 && e.target.checkValidity()) { | ||||||
|  |         e.target.form.submit(); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  | @ -0,0 +1,100 @@ | ||||||
|  | <?php | ||||||
|  | /** | ||||||
|  |  * Combined credentials management template | ||||||
|  |  * Handles both password changes and 2FA management | ||||||
|  |  */ | ||||||
|  | ?>
 | ||||||
|  | 
 | ||||||
|  | <div class="container mt-4"> | ||||||
|  |     <div class="row justify-content-center"> | ||||||
|  |         <div class="col-md-8"> | ||||||
|  |             <!-- Password Management --> | ||||||
|  |             <div class="card mb-4"> | ||||||
|  |                 <div class="card-header"> | ||||||
|  |                     <h3>change password</h3> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="card-body"> | ||||||
|  |                     <form method="post" action="?page=credentials&item=password"> | ||||||
|  |                         <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>"> | ||||||
|  | 
 | ||||||
|  |                         <div class="form-group"> | ||||||
|  |                             <label for="current_password">current password</label> | ||||||
|  |                             <input type="password"  | ||||||
|  |                                    class="form-control"  | ||||||
|  |                                    id="current_password"  | ||||||
|  |                                    name="current_password"  | ||||||
|  |                                    required> | ||||||
|  |                         </div> | ||||||
|  | 
 | ||||||
|  |                         <div class="form-group mt-3"> | ||||||
|  |                             <label for="new_password">new password</label> | ||||||
|  |                             <input type="password"  | ||||||
|  |                                    class="form-control"  | ||||||
|  |                                    id="new_password"  | ||||||
|  |                                    name="new_password" | ||||||
|  |                                    pattern=".{8,}" | ||||||
|  |                                    title="Password must be at least 8 characters long" | ||||||
|  |                                    required> | ||||||
|  |                             <small class="form-text text-muted">minimum 8 characters</small> | ||||||
|  |                         </div> | ||||||
|  | 
 | ||||||
|  |                         <div class="form-group mt-3"> | ||||||
|  |                             <label for="confirm_password">confirm new password</label> | ||||||
|  |                             <input type="password"  | ||||||
|  |                                    class="form-control"  | ||||||
|  |                                    id="confirm_password"  | ||||||
|  |                                    name="confirm_password" | ||||||
|  |                                    pattern=".{8,}" | ||||||
|  |                                    required> | ||||||
|  |                         </div> | ||||||
|  | 
 | ||||||
|  |                         <div class="mt-4"> | ||||||
|  |                             <button type="submit" class="btn btn-primary">change password</button> | ||||||
|  |                         </div> | ||||||
|  |                     </form> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  | 
 | ||||||
|  |             <!-- 2FA Management --> | ||||||
|  |             <div class="card"> | ||||||
|  |                 <div class="card-header"> | ||||||
|  |                     <h3>two-factor authentication</h3> | ||||||
|  |                 </div> | ||||||
|  |                 <div class="card-body"> | ||||||
|  |                     <p class="mb-4">Two-factor authentication adds an extra layer of security to your account. Once enabled, you'll need to enter both your password and a code from your authenticator app when signing in.</p> | ||||||
|  | 
 | ||||||
|  |                     <?php if ($has2fa): ?>
 | ||||||
|  |                         <div class="alert alert-success"> | ||||||
|  |                             <i class="fas fa-check-circle"></i> two-factor authentication is enabled | ||||||
|  |                         </div> | ||||||
|  |                         <form method="post" action="?page=credentials&item=2fa&action=disable"> | ||||||
|  |                             <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>"> | ||||||
|  |                             <button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to disable two-factor authentication? This will make your account less secure.')"> | ||||||
|  |                                 disable two-factor authentication | ||||||
|  |                             </button> | ||||||
|  |                         </form> | ||||||
|  |                     <?php else: ?>
 | ||||||
|  |                         <div class="alert alert-warning"> | ||||||
|  |                             <i class="fas fa-exclamation-triangle"></i> two-factor authentication is not enabled | ||||||
|  |                         </div> | ||||||
|  |                         <form method="post" action="?page=credentials&item=2fa&action=setup"> | ||||||
|  |                             <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>"> | ||||||
|  |                             <button type="submit" class="btn btn-primary"> | ||||||
|  |                                 set up two-factor authentication | ||||||
|  |                             </button> | ||||||
|  |                         </form> | ||||||
|  |                     <?php endif; ?>
 | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | document.getElementById('confirm_password').addEventListener('input', function() { | ||||||
|  |     if (this.value !== document.getElementById('new_password').value) { | ||||||
|  |         this.setCustomValidity('Passwords do not match'); | ||||||
|  |     } else { | ||||||
|  |         this.setCustomValidity(''); | ||||||
|  |     } | ||||||
|  | });</script> | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue