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>
393 lines
13 KiB
PHP
Executable File
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);
|
|
}
|
|
}
|
|
} |