Files
rspade_system/app/RSpade/CodeQuality/Support/FileSanitizer.php
root 29c657f7a7 Exclude tests directory from framework publish
Add 100+ automated unit tests from .expect file specifications
Add session system test
Add rsx:constants:regenerate command test
Add rsx:logrotate command test
Add rsx:clean command test
Add rsx:manifest:stats command test
Add model enum system test
Add model mass assignment prevention test
Add rsx:check command test
Add migrate:status command test

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-25 03:59:58 +00:00

393 lines
13 KiB
PHP

<?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);
}
}
}