$comment_line) { if ($idx === 0 && $current_line !== '') { // First line of comment - complete current line $lines[] = $current_line; $current_line = ''; } elseif ($idx > 0) { // Additional comment lines $lines[] = ''; } } } else { // Add non-comment content $content_parts = explode("\n", $token_content); foreach ($content_parts as $idx => $part) { if ($idx > 0) { $lines[] = $current_line; $current_line = $part; } else { $current_line .= $part; } } } } else { // Single character tokens $current_line .= $token; } } // Add the last line if any if ($current_line !== '' || count($lines) === 0) { $lines[] = $current_line; } $sanitized_content = implode("\n", $lines); return [ 'content' => $sanitized_content, 'lines' => $lines, 'original_lines' => explode("\n", $content), ]; } /** * Get sanitized JavaScript content for checking * Removes comments and string contents to avoid false positives * Uses RPC server for performance */ public static function sanitize_javascript(string $file_path): array { // Start RPC server on first use (lazy initialization) if (static::$rpc_server_process === null) { static::start_rpc_server(); } // Create cache directory if it doesn't exist $base_path = function_exists('base_path') ? base_path() : '/var/www/html'; $cache_dir = $base_path . '/' . self::CACHE_DIR; if (!is_dir($cache_dir)) { mkdir($cache_dir, 0755, true); } // Generate cache path based on relative file path $relative_path = str_replace($base_path . '/', '', $file_path); $cache_path = $cache_dir . '/' . str_replace('/', '_', $relative_path) . '.sanitized'; // Check if cache is valid if (file_exists($cache_path)) { $source_mtime = filemtime($file_path); $cache_mtime = filemtime($cache_path); if ($cache_mtime >= $source_mtime) { // Cache is valid, return cached content $sanitized_content = file_get_contents($cache_path); return [ 'content' => $sanitized_content, 'lines' => explode("\n", $sanitized_content), 'original_lines' => explode("\n", file_get_contents($file_path)), ]; } } // Sanitize via RPC server $sanitized = static::_sanitize_via_rpc($file_path); // Save to cache file_put_contents($cache_path, $sanitized); return [ 'content' => $sanitized, 'lines' => explode("\n", $sanitized), 'original_lines' => explode("\n", file_get_contents($file_path)), ]; } /** * Sanitize via RPC server */ protected static function _sanitize_via_rpc($file_path): string { $base_path = function_exists('base_path') ? base_path() : '/var/www/html'; $socket_path = $base_path . '/' . self::RPC_SOCKET; try { $sock = @stream_socket_client("unix://{$socket_path}", $errno, $errstr, 0.5); if (!$sock) { throw new \RuntimeException("Failed to connect to RPC server: {$errstr}"); } // Send sanitize request $request = [ 'id' => ++static::$request_id, 'method' => 'sanitize', 'files' => [$file_path] ]; fwrite($sock, json_encode($request) . "\n"); // Read response $response = fgets($sock); fclose($sock); if (!$response) { throw new \RuntimeException("No response from RPC server"); } $data = json_decode($response, true); if (!$data || !is_array($data)) { throw new \RuntimeException("Invalid JSON response from RPC server"); } if (isset($data['error'])) { throw new \RuntimeException("RPC error: " . $data['error']); } if (!isset($data['results'][$file_path])) { throw new \RuntimeException("No result for file in RPC response"); } $result = $data['results'][$file_path]; // Handle sanitize result if ($result['status'] === 'success') { return $result['sanitized']; } // Handle error response if ($result['status'] === 'error' && isset($result['error'])) { $error = $result['error']; $message = $error['message'] ?? 'Unknown error'; throw new \RuntimeException("Sanitization error: " . $message); } // Unknown response format throw new \RuntimeException( "JavaScript sanitizer RPC returned unexpected response for {$file_path}:\n" . json_encode($result, JSON_PRETTY_PRINT) ); } catch (\Exception $e) { // Wrap exceptions throw new \RuntimeException( "JavaScript sanitizer RPC error for {$file_path}: " . $e->getMessage() ); } } /** * Sanitize file based on extension * Note: PHP takes content, JavaScript takes file_path (matching monolith behavior) */ public static function sanitize(string $file_path, ?string $content = null): array { $extension = pathinfo($file_path, PATHINFO_EXTENSION); if ($extension === 'php') { if ($content === null) { $content = file_get_contents($file_path); } return self::sanitize_php($content); } elseif (in_array($extension, ['js', 'jsx', 'ts', 'tsx'])) { // JavaScript sanitization needs file path, not content return self::sanitize_javascript($file_path); } else { // For other files, return as-is if ($content === null) { $content = file_get_contents($file_path); } return [ 'content' => $content, 'lines' => explode("\n", $content), 'original_lines' => explode("\n", $content), ]; } } /** * Start the RPC server */ public static function start_rpc_server(): void { // Check if server already running $base_path = function_exists('base_path') ? base_path() : '/var/www/html'; $socket_path = $base_path . '/' . self::RPC_SOCKET; if (file_exists($socket_path)) { // Server might be running, force stop it if (function_exists('console_debug')) { console_debug('JS_SANITIZER', 'Found existing socket, forcing shutdown'); } static::stop_rpc_server(force: true); } // Start new server $server_script = $base_path . '/' . self::RPC_SERVER_SCRIPT; if (function_exists('console_debug')) { console_debug('JS_SANITIZER', 'Starting RPC server: ' . $server_script); } $process = new Process([ 'node', $server_script, '--socket=' . $socket_path ]); $process->start(); static::$rpc_server_process = $process; // Register shutdown handler register_shutdown_function([self::class, 'stop_rpc_server']); // Wait for server to be ready (ping/pong up to 10 seconds) if (function_exists('console_debug')) { console_debug('JS_SANITIZER', 'Waiting for RPC server to be ready...'); } $max_attempts = 200; // 10 seconds (50ms * 200) $ready = false; for ($i = 0; $i < $max_attempts; $i++) { usleep(50000); // 50ms if (file_exists($socket_path)) { // Try to ping if (static::ping_rpc_server()) { $ready = true; if (function_exists('console_debug')) { console_debug('JS_SANITIZER', 'RPC server ready after ' . ($i * 50) . 'ms'); } break; } } } if (!$ready) { static::stop_rpc_server(); throw new \RuntimeException('Failed to start JS Sanitizer RPC server - timeout after 10 seconds'); } if (function_exists('console_debug')) { console_debug('JS_SANITIZER', 'RPC server started successfully'); } } /** * Ping the RPC server */ protected static function ping_rpc_server(): bool { $base_path = function_exists('base_path') ? base_path() : '/var/www/html'; $socket_path = $base_path . '/' . self::RPC_SOCKET; try { $sock = @stream_socket_client("unix://{$socket_path}", $errno, $errstr, 0.5); if (!$sock) { return false; } // Send ping fwrite($sock, json_encode(['id' => ++static::$request_id, 'method' => 'ping']) . "\n"); // Read response $response = fgets($sock); fclose($sock); if (!$response) { return false; } $data = json_decode($response, true); return isset($data['result']) && $data['result'] === 'pong'; } catch (\Exception $e) { return false; } } /** * Stop the RPC server */ public static function stop_rpc_server(bool $force = false): void { $base_path = function_exists('base_path') ? base_path() : '/var/www/html'; $socket_path = $base_path . '/' . self::RPC_SOCKET; // Try graceful shutdown if (file_exists($socket_path)) { try { $sock = @stream_socket_client("unix://{$socket_path}", $errno, $errstr, 0.5); if ($sock) { fwrite($sock, json_encode(['id' => 0, 'method' => 'shutdown']) . "\n"); fclose($sock); } } catch (\Exception $e) { // Ignore errors } } // Only wait and force kill if $force = true if ($force && static::$rpc_server_process && static::$rpc_server_process->isRunning()) { if (function_exists('console_debug')) { console_debug('JS_SANITIZER', 'Force stopping RPC server'); } // Wait for graceful shutdown sleep(1); // Force kill if still running if (static::$rpc_server_process->isRunning()) { static::$rpc_server_process->stop(3, SIGTERM); } } // Clean up socket file if (file_exists($socket_path)) { @unlink($socket_path); } } }