$meta) { // Process and detect if actual change occurred if (processDomain($name, $meta)) { // Reset 'changed' flag after processing $domains[$name]['changed'] = 0; $changed = true; } } // Restart nginx if any updates were made and save new state if ($changed) { shell_exec("sudo nginx -t && sudo systemctl reload nginx"); echo "Reloaded nginx configuration.\n"; // Save updated meta.json back to file if (!saveJson(METAFILE, $domains)) { exit(1); } } echo "All applicable domains processed.\n"; } /** * Generic processor for a location block in NGINX config lines. * Applies a callback to each line inside the matched location block. * * @param array $lines NGINX config lines * @param string $path Location path to match (e.g. "/api/", "\.php$") * @param string|null $modifier Location modifier (e.g. "~", "^~", "=", or null) * @param callable $callback Callback: function(array &$lines, int $i): void * @return array Modified lines */ function processLocationBlock(array $lines, string $path, ?string $modifier, callable $callback): array { $inBlock = false; $braceLevel = 0; $escapedPath = preg_quote($path, '/'); if ($modifier !== null) { $pattern = "/^\\s*location\\s+{$escapedPath}\\s+$modifier\\s*\\{/"; // e.g. location /api/ ^~ { } else { $pattern = "/^\\s*location\\s+{$escapedPath}\\s*\\{/"; // e.g. location /api/ { } foreach ($lines as $i => $line) { if (!$inBlock && preg_match($pattern, $line)) { $inBlock = true; $braceLevel = substr_count($line, '{') - substr_count($line, '}'); $callback($lines, $i, 'start'); continue; } if ($inBlock) { $braceLevel += substr_count($line, '{'); $braceLevel -= substr_count($line, '}'); $callback($lines, $i, 'inside'); if ($braceLevel <= 0) { $callback($lines, $i, 'end'); $inBlock = false; } } } return $lines; } /** * Removes a specific `location` block from an array of NGINX configuration lines. * * This function uses a helper (`processLocationBlock`) to identify a `location` block * by its path and optional modifier (e.g., `~`, `=`, `^~`), and removes the entire block * including its contents from the provided config lines. * * @param array &$lines NGINX config lines. * @param string $path The location path to match (e.g., "/api", "\.php$"). * @param string|null $modifier Optional location modifier (e.g. `~`, `=`, `^~`). If null, matches unmodified location. * @return array A new array of config lines with the matched block removed. */ function removeLocationBlock(array $lines, string $path, ?string $modifier = null): array { $result = []; // Final result array for cleaned config lines $skipBlock = false; // Flag indicating whether we are inside a block to remove $buffer = []; // Temporary buffer for skipped lines (optional, unused here) // Process the location block using a callback to mark lines for deletion $lines = processLocationBlock($lines, $path, $modifier, function(array &$lines, int $i, string $phase) use (&$skipBlock, &$buffer) { if ($phase === 'start') { $skipBlock = true; // Begin removing lines at block start } if ($skipBlock) { $lines[$i] = null; // Mark current line for removal } if ($phase === 'end') { $skipBlock = false; // Stop skipping lines at block end } }); // Remove all lines marked as null (deleted) and reindex foreach ($lines as $line) { if (!isEmpty($line)) { $result[] = $line; } } return $result; } /** * Updates one or more NGINX directives inside a specific location block. * Also optionally appends the directive if it's not found in the block. * * @param array $lines NGINX config lines as an array * @param string $locationPath The location path to match (e.g. "/api/") * @param string|null $modifier Modifier for location (e.g. "~", "^~") or null for plain * @param array $replacements Array of directive replacements in the format: * [ * [ * 'name' => 'directive_name', * 'val' => 'replacement value', * 'append' => true|false (optional) * ], * ... * ] * @return array Modified lines with replacements and optional appends */ function updateLocationBlock(array $lines, string $locationPath, ?string $modifier, array $replacements): array { $seenDirectives = []; // Tracks which directives were matched and replaced return processLocationBlock($lines, $locationPath, $modifier, function(array &$lines, int $i, string $phase) use ($replacements, &$seenDirectives) { // This block runs for each line inside the matching location block if ($phase === 'inside') { foreach ($replacements as $rep) { // Validate required fields if (!isset($rep['name'], $rep['val'])) continue; $name = preg_quote($rep['name'], '/'); // Escape directive name for regex $val = $rep['val']; // If line contains the directive, replace its value if (preg_match("/^\\s*{$name}\\s+.+;/", $lines[$i])) { $lines[$i] = preg_replace("/^\\s*{$name}\\s+.+;/", "{$rep['name']} {$val};", $lines[$i]); $seenDirectives[$rep['name']] = true; // Mark directive as seen } } } // At the end of the location block, insert any missing directives marked for appending if ($phase === 'end') { $inserts = []; foreach ($replacements as $rep) { $append = $rep['append'] ?? false; if (!isset($rep['name'], $rep['val']) || !$append) continue; // If directive wasn't seen inside the block, add it now if (empty($seenDirectives[$rep['name']])) { $inserts[] = " {$rep['name']} {$rep['val']};"; // Indentation is preserved } } // Insert new lines right before the closing brace of the location block if (!empty($inserts)) { array_splice($lines, $i, 0, $inserts); } } }); } /** * Processes a single domain configuration from meta.json. * * - Verifies all required fields are present and non-empty. * - Constructs the expected proxy_pass value from protocol, domain, and port. * - Replaces only the proxy_pass directive within the specified location block. * - If the config changes, it is saved and NGINX will be marked for reload. * - Even if no actual config differences are found, a restart can still be triggered * by manually setting `"changed": 1` in meta.json. * * @param string $name The key in meta.json (usually target vhost name) * @param array $meta The domain's metadata including IP, protocol, port, location, etc. * @return bool True if a reload should be performed, false otherwise */ function processDomain(string $name, array $meta): bool { $requiredFields = ['domain', 'ip', 'protocol', 'port', 'location']; // Required keys $missing = []; $result = false; // Collect any missing or empty required fields foreach ($requiredFields as $field) { if (isEmpty($meta[$field])) { $missing[] = $field; } } // Skip this domain if required fields are missing if (!isEmpty($missing)) { echo "Skipping '$name': missing or empty field(s): " . implode(', ', $missing) . "\n"; return false; } // Allow forced reloads even without content change by setting "changed": 1 in meta.json $result = boolval($meta['changed'] ?? 1); // Full path to nginx config file for this domain $systemConfPath = VHOST_BASE . '/' . $name . '/conf/' . NGINX_CONF_NAME; // Check file existence and permissions if (!file_exists($systemConfPath)) { echo "System config not found for $name\n"; return false; } if (!is_readable($systemConfPath) || !is_writable($systemConfPath)) { echo "Permission denied for $systemConfPath\n"; return false; } // Read current nginx config $original = file_get_contents($systemConfPath); $lines = explode("\n", $original); // Split config into lines // Build new proxy_pass target $proxyTarget = $meta['protocol'] . '://' . $meta['domain'] . ':' . $meta['port']; // Apply transformation inside matching location block $lines = updateLocationBlock($lines, $meta['location'], null, [['name' => 'proxy_pass', 'val' => $proxyTarget]]); // Remove the PHP handler location block from nginx config. // This prevents local PHP processing and ensures the request is forwarded to the remote upstream server. $lines = removeLocationBlock($lines, '~', '.*\.php.*'); $modified = implode("\n", $lines); // Write only if content changed if ($original !== $modified) { $result = (file_put_contents($systemConfPath, $modified) !== false); echo "Domain: $name is updated\n"; } return $result; }