Files
rspade_system/app/RSpade/CodeQuality/Support/FileSanitizer.php
root 77b4d10af8 Refactor filename naming system and apply convention-based renames
Standardize settings file naming and relocate documentation files
Fix code quality violations from rsx:check
Reorganize user_management directory into logical subdirectories
Move Quill Bundle to core and align with Tom Select pattern
Simplify Site Settings page to focus on core site information
Complete Phase 5: Multi-tenant authentication with login flow and site selection
Add route query parameter rule and synchronize filename validation logic
Fix critical bug in UpdateNpmCommand causing missing JavaScript stubs
Implement filename convention rule and resolve VS Code auto-rename conflict
Implement js-sanitizer RPC server to eliminate 900+ Node.js process spawns
Implement RPC server architecture for JavaScript parsing
WIP: Add RPC server infrastructure for JS parsing (partial implementation)
Update jqhtml terminology from destroy to stop, fix datagrid DOM preservation
Add JQHTML-CLASS-01 rule and fix redundant class names
Improve code quality rules and resolve violations
Remove legacy fatal error format in favor of unified 'fatal' error type
Filter internal keys from window.rsxapp output
Update button styling and comprehensive form/modal documentation
Add conditional fly-in animation for modals
Fix non-deterministic bundle compilation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 19:10:02 +00:00

393 lines
13 KiB
PHP
Executable File

<?php
namespace App\RSpade\CodeQuality\Support;
use Symfony\Component\Process\Process;
class FileSanitizer
{
/**
* Node.js RPC server script path
*/
protected const RPC_SERVER_SCRIPT = 'app/RSpade/CodeQuality/Support/resource/js-sanitizer-server.js';
/**
* Unix socket path for RPC server
*/
protected const RPC_SOCKET = 'storage/rsx-tmp/js-sanitizer-server.sock';
/**
* Cache directory for sanitized JavaScript files
*/
protected const CACHE_DIR = 'storage/rsx-tmp/cache/js-sanitized';
/**
* RPC server process
*/
protected static $rpc_server_process = null;
/**
* Request ID counter
*/
protected static $request_id = 0;
/**
* Get PHP content with comments removed
* This ensures we don't match patterns inside comments
* Uses PHP tokenizer to properly strip comments (from line 711 of monolith)
*/
public static function sanitize_php(string $content): array
{
// Use PHP tokenizer to properly strip comments
$tokens = token_get_all($content);
$lines = [];
$current_line = '';
foreach ($tokens as $token) {
if (is_array($token)) {
$token_type = $token[0];
$token_content = $token[1];
// Skip comment tokens
if ($token_type === T_COMMENT || $token_type === T_DOC_COMMENT) {
// Add empty lines to preserve line numbers
$comment_lines = explode("\n", $token_content);
foreach ($comment_lines as $idx => $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);
}
}
}