$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; } function removeLocationBlock(array &$lines, string $path, ?string $modifier = null): array { $result = []; $skipBlock = false; $buffer = []; $lines = processLocationBlock($lines, $path, $modifier, function(array &$lines, int $i, string $phase) use (&$skipBlock, &$buffer) { if ($phase === 'start') { $skipBlock = true; } if ($skipBlock) { $lines[$i] = null; // Mark for removal } if ($phase === 'end') { $skipBlock = false; } }); // Filter out null lines foreach ($lines as $line) { if (!isEmpty($line)) $result[] = $line; } return $result; } /** * Updates one or more directives (e.g. proxy_pass) inside a given location block. * * @param array $lines Array of nginx configuration lines. * @param string $locationPath The location path to match (e.g. "/api/"). * @param string|null $modifier Optional modifier (e.g. "~", "^~"). * @param array $replacements List of replacements: [['name' => 'proxy_pass', 'val' => 'http://...'], ...] * @return array Modified nginx config lines. */ function updateLocationBlock(array $lines, string $locationPath, ?string $modifier, array $replacements): array { return processLocationBlock($lines, $locationPath, $modifier, function(array &$lines, int $i, string $phase) use ($replacements) { if ($phase === 'inside') { foreach ($replacements as $rep) { if (!isset($rep['name'], $rep['val'])) continue; $name = preg_quote($rep['name'], '/'); $val = $rep['val']; if (preg_match("/$name\\s+.+;/", $lines[$i])) { $lines[$i] = preg_replace("/$name\\s+.+;/", "$name $val;", $lines[$i]); } } } }); } /** * 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 from nginx php обработчик так как он приводит к обработке локально, а не на удаленном сервере $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; }