diff --git a/app/classes/api_response.php b/app/classes/api_response.php new file mode 100644 index 0000000..39bc280 --- /dev/null +++ b/app/classes/api_response.php @@ -0,0 +1,47 @@ + 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; + } +} diff --git a/app/classes/config.php b/app/classes/config.php index 95eeae0..9ccb278 100644 --- a/app/classes/config.php +++ b/app/classes/config.php @@ -3,7 +3,7 @@ /** * class Config * - * Handles editing and fetching ot the config files. + * Handles editing and fetching of the config files. */ class Config { @@ -13,41 +13,146 @@ class Config { * @param array $updatedConfig Key-value pairs of config options to update. * @param string $config_file Path to the config file. * - * @return mixed Returns true on success, or an error message on failure. + * @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) { - // first we get a fresh config file contents as text - $config_contents = file_get_contents($config_file); - if (!$config_contents) { - return "Failed to read the config file \"$config_file\"."; - } + global $logObject, $user_id; + $allLogs = []; + $updated = []; - // loop through the variables and updated them - foreach ($updatedConfig as $key => $newValue) { - // we look for 'option' => value - // option is always in single quotes - // value is without quotes, because it could be true/false - $pattern = "/(['\"]{$key}['\"]\s*=>\s*)([^,]+),/"; - - // prepare the value, make booleans w/out single quotes - if ($newValue === 'true') { - $replacementValue = 'true'; - } elseif ($newValue === 'false') { - $replacementValue = 'false'; - } else { - $replacementValue = var_export($newValue, true); + try { + if (!is_array($updatedConfig)) { + throw new Exception("Invalid config data: expected array"); } - // value replacing - $config_contents = preg_replace($pattern, "$1{$replacementValue},", $config_contents); - } + if (!file_exists($config_file) || !is_writable($config_file)) { + 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\"."; - } + // 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"); + } - return true; + $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->insertLog($user_id, implode("\n", $allLogs), 'system'); + } + + return [ + 'success' => true, + 'updated' => $updated + ]; + } catch (Exception $e) { + $logObject->insertLog($user_id, "Config update error: " . $e->getMessage(), 'system'); + return [ + 'success' => false, + 'error' => $e->getMessage() + ]; + } } - } diff --git a/app/config/jilo-web.conf.php b/app/config/jilo-web.conf.php index 123371b..5550527 100644 --- a/app/config/jilo-web.conf.php +++ b/app/config/jilo-web.conf.php @@ -13,7 +13,7 @@ return [ // site name used in emails and in the inteerface 'site_name' => 'Jilo Web', // set to false to disable new registrations - 'registration_enabled' => '1', + 'registration_enabled' => true, // will be displayed on login screen 'login_message' => '', diff --git a/app/includes/csrf_middleware.php b/app/includes/csrf_middleware.php index bf5317e..e69b745 100644 --- a/app/includes/csrf_middleware.php +++ b/app/includes/csrf_middleware.php @@ -4,6 +4,7 @@ require_once __DIR__ . '/../helpers/security.php'; require_once __DIR__ . '/../helpers/logs.php'; function applyCsrfMiddleware() { + global $logObject; $security = SecurityHelper::getInstance(); // Skip CSRF check for GET requests @@ -21,7 +22,8 @@ function applyCsrfMiddleware() { // Check CSRF token for all other POST requests if ($_SERVER['REQUEST_METHOD'] === 'POST') { - $token = $_POST['csrf_token'] ?? ''; + // 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 = getUserIP(); @@ -31,7 +33,7 @@ function applyCsrfMiddleware() { $_GET['page'] ?? 'unknown', $_SESSION['username'] ?? 'anonymous' ); - $logObject->insertLog(0, $logMessage, 'system'); + $logObject->insertLog(null, $logMessage, 'system'); // Return error message http_response_code(403); diff --git a/app/pages/config.php b/app/pages/config.php index 256c223..ac97055 100644 --- a/app/pages/config.php +++ b/app/pages/config.php @@ -10,14 +10,28 @@ include '../app/helpers/feedback.php'; require '../app/classes/config.php'; -$configObject = new Config(); +require '../app/classes/api_response.php'; -require '../app/includes/rate_limit_middleware.php'; +// Initialize required objects +$userObject = new User($dbWeb); +$logObject = new Log($dbWeb); +$configObject = new Config(); // For AJAX requests $isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'; +// Ensure config file path is set +if (!isset($config_file) || empty($config_file)) { + if ($isAjax) { + ApiResponse::error('Config file path not set'); + } else { + Feedback::flash('ERROR', 'DEFAULT', 'Config file path not set'); + header('Location: ' . htmlspecialchars($app_root)); + } + exit; +} + // Check if file is writable $isWritable = is_writable($config_file); $configMessage = ''; @@ -26,7 +40,19 @@ if (!$isWritable) { } if ($_SERVER['REQUEST_METHOD'] === 'POST') { + // Check if user has permission to edit config + if (!$userObject->hasRight($user_id, 'edit config file')) { + $logObject->insertLog($user_id, "Unauthorized: User \"$currentUser\" tried to edit config file. IP: $user_IP", 'system'); + if ($isAjax) { + ApiResponse::error('Forbidden: You do not have permission to edit the config file', null, 403); + } else { + include '../app/templates/error-unauthorized.php'; + } + exit; + } + // Apply rate limiting + require '../app/includes/rate_limit_middleware.php'; checkRateLimit($dbWeb, 'config', $user_id); // Ensure no output before this point @@ -34,50 +60,35 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // For AJAX requests, get JSON data if ($isAjax) { - header('Content-Type: application/json'); - // Get raw input $jsonData = file_get_contents('php://input'); + if ($jsonData === false) { + $logObject->insertLog($user_id, "Failed to read request data for config update", 'system'); + ApiResponse::error('Failed to read request data'); + } + // Try to parse JSON $postData = json_decode($jsonData, true); - if (json_last_error() !== JSON_ERROR_NONE) { $error = json_last_error_msg(); - - Feedback::flash('ERROR', 'DEFAULT', 'Invalid JSON data received: ' . $error, true); - echo json_encode([ - 'success' => false, - 'message' => 'Invalid JSON data received: ' . $error - ]); - exit; + ApiResponse::error('Invalid JSON data received: ' . $error); } // Try to update config file $result = $configObject->editConfigFile($postData, $config_file); - if ($result === true) { - $messageData = Feedback::getMessageData('NOTICE', 'DEFAULT', 'Config file updated successfully', true); - echo json_encode([ - 'success' => true, - 'message' => 'Config file updated successfully', - 'messageData' => $messageData - ]); + if ($result['success']) { + ApiResponse::success($result['updated'], 'Config file updated successfully'); } else { - $messageData = Feedback::getMessageData('ERROR', 'DEFAULT', "Error updating config file: $result", true); - echo json_encode([ - 'success' => false, - 'message' => "Error updating config file: $result", - 'messageData' => $messageData - ]); + ApiResponse::error($result['error']); } - exit; } // Handle non-AJAX POST $result = $configObject->editConfigFile($_POST, $config_file); - if ($result === true) { + if ($result['success']) { Feedback::flash('NOTICE', 'DEFAULT', 'Config file updated successfully', true); } else { - Feedback::flash('ERROR', 'DEFAULT', "Error updating config file: $result", true); + Feedback::flash('ERROR', 'DEFAULT', "Error updating config file: " . $result['error'], true); } header('Location: ' . htmlspecialchars($app_root) . '?page=config'); diff --git a/app/templates/config.php b/app/templates/config.php index 097caca..2d15a36 100644 --- a/app/templates/config.php +++ b/app/templates/config.php @@ -1,42 +1,44 @@ - -
-
-
-

Configuration

- Jilo Web configuration file: + +
+
+
+

Configuration

+ configuration file: - + -
-
+
+
-
-
-
- - Jilo Web app configuration -
+
+
+
+ + app configuration +
hasRight($user_id, 'edit config file')) { ?> -
- -
- - -
-
+
+ +
+ + +
+
-
+
-
-
+
+ \n"; } else { ?> -
-
- -
-
-
+
+
+ +
+
+
- + - + - blank + blank - + -
-
+
+
-
- > -
+
+ > +
- + - + -
-
-
- $value) { - renderConfigItem($key, $value); - } ?> -
-
+ $value) { + renderConfigItem($key, $value); + } ?> + +
+
+
+