Files
rspade_system/app/RSpade/Ide/Services/handler.php
root 37a6183eb4 Fix code quality violations and add VS Code extension features
Fix VS Code extension storage paths for new directory structure
Fix jqhtml compiled files missing from bundle
Fix bundle babel transformation and add rsxrealpath() function

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-22 00:43:05 +00:00

1253 lines
42 KiB
PHP
Executable File

<?php
/**
* IDE Service Handler
*
* Handles all IDE service requests bypassing Laravel for performance.
* Uses file-based authentication with SHA1 message signing.
*
* Authentication Flow:
* 1. VS Code generates session, client_key, server_key and writes to auth.json
* 2. Client signs requests with SHA1(body + client_key)
* 3. Server validates client signature
* 4. Server signs response with SHA1(body + server_key)
* 5. Client validates server signature
*/
// Error reporting for development
error_reporting(E_ALL);
ini_set('display_errors', 0);
// Base path - framework is in system/ subdirectory
// @REALPATH-EXCEPTION - Bootstrap file: runs before helpers loaded, needs PHP's realpath()
$system_path = realpath(__DIR__ . '/../../../..');
// @REALPATH-EXCEPTION - Bootstrap file: runs before helpers loaded, needs PHP's realpath()
define('IDE_BASE_PATH', realpath($system_path . '/..')); // Project root
define('IDE_SYSTEM_PATH', $system_path); // Framework root
// Helper to get framework paths
function ide_framework_path($relative_path) {
return IDE_SYSTEM_PATH . '/' . ltrim($relative_path, '/');
}
// Define Laravel helper that exec_safe() needs
// This standalone handler never runs with Laravel, so no conflict
function storage_path($path = '') {
$base = IDE_SYSTEM_PATH . '/storage';
return $path ? $base . '/' . ltrim($path, '/') : $base;
}
// Load exec_safe() function
require_once ide_framework_path('app/RSpade/helpers.php');
// JSON response helper
function json_response($data, $code = 200) {
http_response_code($code);
header('Content-Type: application/json');
$json = json_encode($data);
// Sign response if we have auth
global $auth_data;
if (!empty($auth_data['server_key'])) {
$signature = sha1($json . $auth_data['server_key']);
header('X-Signature: ' . $signature);
}
echo $json;
exit;
}
// Error response helper
function error_response($message, $code = 400) {
json_response(['success' => false, 'error' => $message], $code);
}
// Check if IDE services are enabled
$env_file = IDE_BASE_PATH . '/.env';
if (file_exists($env_file)) {
$env_content = file_get_contents($env_file);
// Check if production
if (preg_match('/^APP_ENV=production$/m', $env_content)) {
// Check if explicitly enabled in production
if (!preg_match('/^RSX_IDE_SERVICES_ENABLED=true$/m', $env_content)) {
error_response('IDE services disabled in production', 403);
}
}
}
// Parse request URI to get service
$request_uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$service_path = str_replace('/_ide/service', '', $request_uri);
$service_path = trim($service_path, '/');
// Get request body
$request_body = file_get_contents('php://input');
$request_data = json_decode($request_body, true);
// Merge GET parameters (for backward compatibility with /_idehelper endpoints)
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$request_data = array_merge($request_data ?? [], $_GET);
}
// Handle authentication
$auth_data = [];
$session_header = $_SERVER['HTTP_X_SESSION'] ?? null;
$signature_header = $_SERVER['HTTP_X_SIGNATURE'] ?? null;
if ($session_header && $signature_header) {
// Load auth data for session
$auth_file = ide_framework_path('storage/rsx-ide-bridge/auth-' . $session_header . '.json');
if (file_exists($auth_file)) {
$auth_data = json_decode(file_get_contents($auth_file), true);
// Validate signature
$expected_signature = sha1($request_body . $auth_data['client_key']);
if ($signature_header !== $expected_signature) {
error_response('Invalid signature', 401);
}
} else {
// Session not found - provide recovery hint
http_response_code(401);
header('Content-Type: application/json');
echo json_encode([
'error' => 'Session not found',
'code' => 401,
'recoverable' => true,
'recovery' => 'Create new session via POST /_ide/service/auth/create'
]);
exit;
}
} else {
// No auth provided - check if this is an auth creation request
if ($service_path === 'auth/create' && $_SERVER['REQUEST_METHOD'] === 'POST') {
// Generate new auth session
$session = bin2hex(random_bytes(20));
$client_key = bin2hex(random_bytes(20));
$server_key = bin2hex(random_bytes(20));
$auth_data = [
'session' => $session,
'client_key' => $client_key,
'server_key' => $server_key,
'created' => time()
];
// Save auth file
$bridge_dir = ide_framework_path('storage/rsx-ide-bridge');
if (!is_dir($bridge_dir)) {
mkdir($bridge_dir, 0755, true);
}
$auth_file = $bridge_dir . '/auth-' . $session . '.json';
file_put_contents($auth_file, json_encode($auth_data, JSON_PRETTY_PRINT));
// Return auth data (unsigned response since client doesn't have keys yet)
http_response_code(200);
header('Content-Type: application/json');
echo json_encode([
'success' => true,
'session' => $session,
'client_key' => $client_key,
'server_key' => $server_key
]);
exit;
} else {
error_response('Authentication required', 401);
}
}
// Route to appropriate service handler
switch ($service_path) {
case 'format':
handle_format_service($request_data);
break;
case 'definition':
handle_definition_service($request_data);
break;
case 'complete':
handle_complete_service($request_data);
break;
case 'exec':
handle_exec_service($request_data);
break;
case 'git':
handle_git_service($request_data);
break;
case 'git/diff':
handle_git_diff_service($request_data);
break;
case 'command':
handle_command_service($request_data);
break;
case 'health':
json_response(['success' => true, 'service' => 'ide', 'version' => '1.0.0']);
break;
case 'resolve_class':
handle_resolve_class_service($request_data);
break;
case 'js_lineage':
handle_js_lineage_service($request_data);
break;
case 'resolve_url':
handle_resolve_url_service($request_data);
break;
default:
error_response('Unknown service: ' . $service_path, 404);
}
/**
* Handle format service - formats PHP files
*/
function handle_format_service($data) {
$file = $data['file'] ?? null;
if (!$file) {
error_response('No file provided');
}
// Normalize path separators (Windows sends backslashes, convert to forward slashes)
$file = str_replace('\\', '/', $file);
// Build full path
$full_path = IDE_BASE_PATH . '/' . ltrim($file, '/');
// Normalize path without requiring file to exist (for .formatting.tmp files)
// Use realpath on the directory, then append the filename
$dir = dirname($full_path);
$filename = basename($full_path);
// Get real directory path
if (is_dir($dir)) {
// @REALPATH-EXCEPTION - IDE service: runs standalone without framework helpers
$real_dir = realpath($dir);
if ($real_dir) {
$full_path = $real_dir . '/' . $filename;
}
}
// Security check - path must be within project
if (strpos($full_path, IDE_BASE_PATH) !== 0) {
error_response('Invalid file path - must be within project');
}
if (!file_exists($full_path)) {
error_response('File not found', 404);
}
// Execute formatter
$formatter_path = ide_framework_path('bin/rsx-format');
if (!file_exists($formatter_path)) {
error_response('Formatter not found', 500);
}
// Log formatting request
$log_file = ide_framework_path('storage/logs/ide-formatter.log');
$log_dir = dirname($log_file);
if (!is_dir($log_dir)) {
mkdir($log_dir, 0755, true);
}
$log_entry = sprintf(
"[%s] Formatting file: %s\n Full path: %s\n CWD before: %s\n File exists: %s\n File size: %d bytes\n",
date('Y-m-d H:i:s'),
$file,
$full_path,
getcwd(),
file_exists($full_path) ? 'yes' : 'no',
file_exists($full_path) ? filesize($full_path) : 0
);
file_put_contents($log_file, $log_entry, FILE_APPEND);
// Change to project root so formatter can find config files
$old_cwd = getcwd();
chdir(IDE_BASE_PATH);
$command = sprintf('php %s %s 2>&1',
escapeshellarg($formatter_path),
escapeshellarg($full_path)
);
file_put_contents($log_file, " CWD after chdir: " . getcwd() . "\n Command: $command\n", FILE_APPEND);
$output = [];
$return_var = 0;
\exec_safe($command, $output, $return_var);
// Restore working directory
chdir($old_cwd);
// Log result
file_put_contents($log_file, sprintf(
" Return code: %d\n Output lines: %d\n Output: %s\n File size after: %d bytes\n\n",
$return_var,
count($output),
implode("\n ", $output),
file_exists($full_path) ? filesize($full_path) : 0
), FILE_APPEND);
if ($return_var === 0) {
// Read formatted content if requested
$content = null;
if (!empty($data['return_content'])) {
$content = file_get_contents($full_path);
}
json_response([
'success' => true,
'message' => 'Formatting completed',
'content' => $content
]);
} else {
error_response('Formatting failed: ' . implode("\n", $output), 500);
}
}
/**
* Handle definition service - find symbol definitions
* Supports all the types from the VS Code definition provider
*/
function handle_definition_service($data) {
$identifier = $data['identifier'] ?? $data['symbol'] ?? null;
$method = $data['method'] ?? null;
$type = $data['type'] ?? null;
if (!$identifier) {
error_response('No identifier provided');
}
// Load manifest data
$manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php');
if (!file_exists($manifest_file)) {
error_response('Manifest not found - run php artisan rsx:manifest:build', 500);
}
$manifest_raw = include $manifest_file;
// The manifest structure has the data under 'data' key
$manifest = $manifest_raw['data'] ?? $manifest_raw;
// Search based on type
$result = null;
switch ($type) {
case 'class':
// Search PHP classes with optional method
if (isset($manifest['php']['classes'][$identifier])) {
$class_data = $manifest['php']['classes'][$identifier];
$file = $class_data['file'];
$line = $class_data['line'] ?? 1;
// If method specified, try to find method line number
if ($method && isset($class_data['methods'][$method])) {
$line = $class_data['methods'][$method]['line'] ?? $line;
}
$result = [
'found' => true,
'file' => $file,
'line' => $line
];
}
break;
case 'view':
// Search Blade views by name or rsx_id
foreach ($manifest['blade']['views'] ?? [] as $view) {
if ($view['name'] === $identifier || $view['rsx_id'] === $identifier) {
$result = [
'found' => true,
'file' => $view['file'],
'line' => 1
];
break;
}
}
break;
case 'bundle_alias':
// Search bundle aliases defined in config/rsx.php
$config_file = ide_framework_path('config/rsx.php');
if (file_exists($config_file)) {
$config = include $config_file;
if (isset($config['bundle_aliases'][$identifier])) {
$bundle_class = $config['bundle_aliases'][$identifier];
// Extract class name from namespace
$class_name = str_replace('\\', '_', str_replace('App\\RSpade\\', '', $bundle_class));
// Find the bundle class file
if (isset($manifest['php']['classes'][$class_name])) {
$result = [
'found' => true,
'file' => $manifest['php']['classes'][$class_name]['file'],
'line' => $manifest['php']['classes'][$class_name]['line'] ?? 1
];
}
}
}
break;
case 'jqhtml_template':
// Search for .jqhtml template files
foreach ($manifest['jqhtml']['components'] ?? [] as $component) {
if ($component['name'] === $identifier) {
$result = [
'found' => true,
'file' => $component['template_file'],
'line' => 1
];
break;
}
}
break;
case 'jqhtml_class':
// Search for JavaScript class extending Jqhtml_Component
foreach ($manifest['jqhtml']['components'] ?? [] as $component) {
if ($component['name'] === $identifier && isset($component['js_file'])) {
$result = [
'found' => true,
'file' => $component['js_file'],
'line' => $component['js_line'] ?? 1
];
break;
}
}
break;
case 'jqhtml_class_method':
// Search for method in jqhtml component JavaScript class
foreach ($manifest['jqhtml']['components'] ?? [] as $component) {
if ($component['name'] === $identifier && isset($component['js_file'])) {
$js_file = IDE_BASE_PATH . '/' . $component['js_file'];
$line = 1;
// Try to find the method line number
if ($method && file_exists($js_file)) {
$content = file_get_contents($js_file);
$lines = explode("\n", $content);
foreach ($lines as $index => $line_content) {
// Look for method definition patterns
if (preg_match('/^\s*' . preg_quote($method) . '\s*\(/', $line_content) ||
preg_match('/^\s*async\s+' . preg_quote($method) . '\s*\(/', $line_content) ||
preg_match('/^\s*static\s+' . preg_quote($method) . '\s*\(/', $line_content)) {
$line = $index + 1;
break;
}
}
}
$result = [
'found' => true,
'file' => $component['js_file'],
'line' => $line
];
break;
}
}
break;
case 'route':
// Search routes for controller::method
foreach ($manifest['php']['routes'] ?? [] as $route) {
if ($route['class'] . '::' . $route['method'] === $identifier) {
$result = [
'found' => true,
'file' => $route['file'],
'line' => $route['line'] ?? 1
];
break;
}
}
break;
default:
// Try to auto-detect type
// First try as class
if (isset($manifest['php']['classes'][$identifier])) {
$class_data = $manifest['php']['classes'][$identifier];
$result = [
'found' => true,
'file' => $class_data['file'],
'line' => $class_data['line'] ?? 1
];
}
// Try as view
if (!$result) {
foreach ($manifest['blade']['views'] ?? [] as $view) {
if ($view['name'] === $identifier || $view['rsx_id'] === $identifier) {
$result = [
'found' => true,
'file' => $view['file'],
'line' => 1
];
break;
}
}
}
// Try as jqhtml component
if (!$result) {
foreach ($manifest['jqhtml']['components'] ?? [] as $component) {
if ($component['name'] === $identifier) {
$result = [
'found' => true,
'file' => $component['template_file'] ?? $component['js_file'],
'line' => 1
];
break;
}
}
}
}
if ($result) {
json_response($result);
} else {
json_response(['found' => false]);
}
}
/**
* Handle complete service - provide autocomplete suggestions
*/
function handle_complete_service($data) {
$prefix = $data['prefix'] ?? '';
$context = $data['context'] ?? null;
// Load manifest data
$manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php');
if (!file_exists($manifest_file)) {
error_response('Manifest not found', 500);
}
$manifest = include $manifest_file;
$suggestions = [];
// Search PHP classes
foreach ($manifest['php']['classes'] ?? [] as $class_name => $class_data) {
if (stripos($class_name, $prefix) === 0) {
$suggestions[] = [
'label' => $class_name,
'kind' => 'class',
'detail' => $class_data['file']
];
}
}
// Limit results
$suggestions = array_slice($suggestions, 0, 100);
json_response([
'success' => true,
'suggestions' => $suggestions
]);
}
/**
* Handle exec service - execute artisan commands
*/
function handle_exec_service($data) {
$command = $data['command'] ?? null;
if (!$command) {
error_response('No command provided');
}
// Whitelist of allowed commands
$allowed_commands = [
'rsx:check',
'rsx:manifest:build',
'rsx:clean',
'rsx:bundle:compile',
'rsx:routes'
];
// Extract base command
$parts = explode(' ', $command);
$base_command = $parts[0];
if (!in_array($base_command, $allowed_commands)) {
error_response('Command not allowed: ' . $base_command, 403);
}
// Build full artisan command
$artisan_path = IDE_BASE_PATH . '/artisan';
$full_command = sprintf('php %s %s 2>&1',
escapeshellarg($artisan_path),
$command
);
$output = [];
$return_var = 0;
\exec_safe($full_command, $output, $return_var);
json_response([
'success' => $return_var === 0,
'output' => implode("\n", $output),
'exit_code' => $return_var
]);
}
/**
* Handle git service - get git status
*/
function handle_git_service($data) {
$output = [];
$return_var = 0;
\exec_safe('cd ' . escapeshellarg(IDE_BASE_PATH) . ' && git status --porcelain 2>&1', $output, $return_var);
if ($return_var !== 0) {
error_response('Git command failed: ' . implode("\n", $output), 500);
}
$modified = [];
$added = [];
$conflicts = [];
foreach ($output as $line) {
if (strlen($line) < 4) continue;
$status = substr($line, 0, 2);
$file = trim(substr($line, 3));
// Parse git status codes
if (str_contains($status, 'U') || ($status[0] === 'A' && $status[1] === 'A')) {
$conflicts[] = $file;
} elseif ($status[0] === 'A' || $status[1] === 'A' || $status === '??') {
$added[] = $file;
} elseif ($status[0] === 'M' || $status[1] === 'M' || $status[0] === 'R') {
$modified[] = $file;
}
}
json_response([
'success' => true,
'modified' => $modified,
'added' => $added,
'conflicts' => $conflicts
]);
}
/**
* Handle git diff service - get line-level changes for a file
*/
function handle_git_diff_service($data) {
$file = $data['file'] ?? null;
if (!$file) {
error_response('No file provided');
}
// Normalize path separators (Windows sends backslashes, convert to forward slashes)
$file = str_replace('\\', '/', $file);
// Security check - file must be within project
$full_path = IDE_BASE_PATH . '/' . ltrim($file, '/');
// Normalize path without requiring file to exist (handles deleted files in git)
$dir = dirname($full_path);
$filename = basename($full_path);
if (is_dir($dir)) {
// @REALPATH-EXCEPTION - IDE service: runs standalone without framework helpers
$real_dir = realpath($dir);
if ($real_dir) {
$full_path = $real_dir . '/' . $filename;
}
}
// Validate path is within project
if (strpos($full_path, IDE_BASE_PATH) !== 0) {
error_response('Invalid file path');
}
// Note: File existence not required for git diff (might be deleted)
$output = [];
$return_var = 0;
// Get diff with line numbers, no context
\exec_safe('cd ' . escapeshellarg(IDE_BASE_PATH) . ' && git diff HEAD --unified=0 ' . escapeshellarg($file) . ' 2>&1', $output, $return_var);
if ($return_var !== 0) {
error_response('Git diff command failed: ' . implode("\n", $output), 500);
}
$added = [];
$modified = [];
$deleted = [];
foreach ($output as $line) {
// Parse unified diff format: @@ -old_start,old_count +new_start,new_count @@
if (preg_match('/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/', $line, $matches)) {
$old_start = (int)$matches[1];
$old_count = isset($matches[2]) ? (int)$matches[2] : 1;
$new_start = (int)$matches[3];
$new_count = isset($matches[4]) ? (int)$matches[4] : 1;
// Deletion: old_count > 0, new_count = 0
if ($old_count > 0 && $new_count === 0) {
$deleted[] = [$new_start, $new_start];
}
// Addition: old_count = 0, new_count > 0
elseif ($old_count === 0 && $new_count > 0) {
$added[] = [$new_start, $new_start + $new_count - 1];
}
// Modification: both > 0
else {
$modified[] = [$new_start, $new_start + $new_count - 1];
}
}
}
json_response([
'success' => true,
'added' => $added,
'modified' => $modified,
'deleted' => $deleted
]);
}
/**
* Handle command service - execute artisan commands with output capture
*/
function handle_command_service($data) {
$command = $data['command'] ?? null;
$arguments = $data['arguments'] ?? [];
if (!$command) {
error_response('No command provided');
}
// Whitelist of allowed commands
$allowed_commands = [
'rsx:check',
'rsx:manifest:build',
'rsx:clean',
'rsx:bundle:compile',
'rsx:routes',
'rsx:refactor:rename_php_class',
'rsx:refactor:rename_php_class_function',
'rsx:refactor:sort_php_class_functions'
];
if (!in_array($command, $allowed_commands)) {
error_response('Command not allowed: ' . $command, 403);
}
// Build artisan command with arguments
$artisan_path = IDE_BASE_PATH . '/artisan';
$command_parts = [
'php',
escapeshellarg($artisan_path),
$command
];
// Add arguments
foreach ($arguments as $arg) {
$command_parts[] = escapeshellarg($arg);
}
$full_command = implode(' ', $command_parts) . ' 2>&1';
// Change to project root
$old_cwd = getcwd();
chdir(IDE_BASE_PATH);
// Capture output using ob_start
ob_start();
$output = [];
$return_var = 0;
\exec_safe($full_command, $output, $return_var);
$ob_output = ob_get_clean();
// Restore working directory
chdir($old_cwd);
$combined_output = trim($ob_output . "\n" . implode("\n", $output));
json_response([
'success' => $return_var === 0,
'output' => $combined_output,
'exit_code' => $return_var
]);
}
/**
* Handle resolve_class service - resolve class/view/component definitions
* This is a port of the Ide_Helper_Controller::resolve_class() method
*/
function handle_resolve_class_service($data) {
$identifier = $data['class'] ?? $data['identifier'] ?? null;
$method_name = $data['method'] ?? null;
$type = $data['type'] ?? null;
if (!$identifier) {
json_response([
'error' => 'Missing required parameter: class or identifier',
'found' => false,
], 400);
}
// Load manifest data
$manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php');
if (!file_exists($manifest_file)) {
error_response('Manifest not found - run php artisan rsx:manifest:build', 500);
}
$manifest_raw = include $manifest_file;
$manifest_data = $manifest_raw['data'] ?? $manifest_raw;
$files = $manifest_data['files'] ?? [];
// Helper function to find PHP class in manifest
$find_php_class = function($class_name) use ($files) {
foreach ($files as $file_path => $file_data) {
if (isset($file_data['class']) && $file_data['class'] === $class_name) {
return $file_data;
}
}
return null;
};
// Helper function to find view in manifest
$find_view = function($view_name) use ($files) {
foreach ($files as $file_path => $file_data) {
if (isset($file_data['id']) && $file_data['id'] === $view_name) {
return $file_data;
}
}
return null;
};
// Helper function to convert PascalCase to snake_case
$camel_to_snake = function($input) {
$result = preg_replace('/(?<!^)[A-Z]/', '_$0', $input);
return strtolower($result);
};
// Helper function to convert snake_case to PascalCase
$snake_to_pascal = function($input) {
$parts = explode('_', $input);
return implode('_', array_map('ucfirst', $parts));
};
// Priority 1: Try as PHP class
if (!$type || $type === 'class' || preg_match('/^[A-Z][A-Za-z0-9_]*$/', $identifier)) {
$class_data = $find_php_class($identifier);
if ($class_data) {
$file_path = $class_data['file'];
$line_number = 1;
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
$lines = explode("\n", $content);
if ($method_name) {
// Check if method metadata exists
if (isset($class_data['public_static_methods'][$method_name])) {
$method_metadata = $class_data['public_static_methods'][$method_name];
if (isset($method_metadata['line'])) {
$line_number = $method_metadata['line'];
}
} else if (isset($class_data['public_instance_methods'][$method_name])) {
$method_metadata = $class_data['public_instance_methods'][$method_name];
if (isset($method_metadata['line'])) {
$line_number = $method_metadata['line'];
}
}
// If we don't have line number from metadata, search manually
if ($line_number === 1 && !empty($lines)) {
$in_class_or_trait = false;
$brace_count = 0;
foreach ($lines as $index => $line) {
if (preg_match('/^\s*(class|trait)\s+\w+(\s|$)/', $line)) {
$in_class_or_trait = true;
}
if ($in_class_or_trait) {
$brace_count += substr_count($line, '{');
$brace_count -= substr_count($line, '}');
if (preg_match('/^\s*(public|protected|private)?\s*(static\s+)?function\s+' . preg_quote($method_name, '/') . '\s*\(/', $line)) {
$line_number = $index + 1;
break;
}
if ($brace_count === 0 && strpos($line, '}') !== false) {
break;
}
}
}
}
} else {
// Find the line with the class definition
foreach ($lines as $index => $line) {
if (preg_match('/^\s*class\s+' . preg_quote($identifier, '/') . '(\s|$)/', $line)) {
$line_number = $index + 1;
break;
}
}
}
}
$response_data = [
'found' => true,
'type' => 'class',
'file' => $file_path,
'line' => $line_number,
'metadata' => [
'namespace' => $class_data['namespace'] ?? null,
'extends' => $class_data['extends'] ?? null,
'fqcn' => $class_data['fqcn'] ?? null,
],
];
if ($method_name) {
$response_data['method'] = $method_name;
}
json_response($response_data);
}
}
// Priority 2: Try as RSX blade view
if (!$type || $type === 'view' || !preg_match('/Controller$/', $identifier)) {
$view_data = $find_view($identifier);
if ($view_data) {
$file_path = $view_data['file'];
$line_number = 1;
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
$lines = explode("\n", $content);
// Look for @rsx_id('identifier')
foreach ($lines as $index => $line) {
if (preg_match('/@rsx_id\s*\(\s*[\'"]' . preg_quote($identifier, '/') . '[\'"]\s*\)/', $line)) {
$line_number = $index + 1;
break;
}
}
}
json_response([
'found' => true,
'type' => 'view',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
]);
}
}
// Priority 3: Try as bundle alias
if (!$type || $type === 'bundle_alias' || preg_match('/^[a-z0-9]+$/', $identifier)) {
$config_path = IDE_SYSTEM_PATH . '/config/rsx.php';
if (file_exists($config_path)) {
$config = include $config_path;
if (isset($config['bundle_aliases'][$identifier])) {
$bundle_class = $config['bundle_aliases'][$identifier];
$class_parts = explode('\\', $bundle_class);
$class_name = end($class_parts);
$class_data = $find_php_class($class_name);
if ($class_data) {
$file_path = $class_data['file'];
$line_number = 1;
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
$lines = explode("\n", $content);
foreach ($lines as $index => $line) {
if (preg_match('/^\s*class\s+' . preg_quote($class_name, '/') . '(\s|$)/', $line)) {
$line_number = $index + 1;
break;
}
}
}
json_response([
'found' => true,
'type' => 'bundle_alias',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
'resolved_class' => $bundle_class,
]);
}
}
}
}
// Priority 4: jqhtml template files
if ($type === 'jqhtml_template') {
$component_snake = $camel_to_snake($identifier);
foreach ($files as $file_path => $file_data) {
if (str_ends_with($file_path, '.jqhtml')) {
$basename = basename($file_path, '.jqhtml');
if ($basename === $component_snake || $snake_to_pascal($basename) === $identifier) {
$line_number = 1;
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
$lines = explode("\n", $content);
foreach ($lines as $index => $line) {
if (preg_match('/<Define:\s*' . preg_quote($identifier, '/') . '/', $line)) {
$line_number = $index + 1;
break;
}
}
}
json_response([
'found' => true,
'type' => 'jqhtml_template',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
]);
}
}
}
}
// Priority 5: jqhtml JavaScript classes
if ($type === 'jqhtml_class') {
foreach ($files as $file_path => $file_data) {
if (str_ends_with($file_path, '.js')) {
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends\s+[A-Za-z_]*Jqhtml_Component/', $content)) {
$lines = explode("\n", $content);
$line_number = 1;
foreach ($lines as $index => $line) {
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends/', $line)) {
$line_number = $index + 1;
break;
}
}
json_response([
'found' => true,
'type' => 'jqhtml_class',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
]);
}
}
}
}
}
// Priority 6: jqhtml class methods
if ($type === 'jqhtml_class_method' && $method_name) {
$search_method = ($method_name === 'data') ? 'on_load' : $method_name;
foreach ($files as $file_path => $file_data) {
if (str_ends_with($file_path, '.js')) {
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends\s+[A-Za-z_]*Jqhtml_Component/', $content)) {
$lines = explode("\n", $content);
$line_number = 1;
$in_class = false;
$brace_count = 0;
foreach ($lines as $index => $line) {
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends/', $line)) {
$in_class = true;
}
if ($in_class) {
$brace_count += substr_count($line, '{');
$brace_count -= substr_count($line, '}');
if (preg_match('/(?:async\s+)?' . preg_quote($search_method, '/') . '\s*\(/', $line)) {
$line_number = $index + 1;
break;
}
if ($brace_count === 0 && strpos($line, '}') !== false) {
break;
}
}
}
json_response([
'found' => true,
'type' => 'jqhtml_class_method',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
'method' => $method_name,
]);
}
}
}
}
}
// Nothing found
json_response([
'found' => false,
'error' => 'Identifier not found in manifest',
'identifier' => $identifier,
'searched_types' => ['class', 'view', 'bundle_alias', 'jqhtml_template', 'jqhtml_class', 'jqhtml_class_method'],
]);
}
/**
* Handle js_lineage service - get JavaScript class inheritance chain
* This is a port of the Ide_Helper_Controller::js_lineage() method
*/
function handle_js_lineage_service($data) {
$class_name = $data['class'] ?? null;
if (!$class_name) {
json_response([
'error' => 'Missing required parameter: class',
], 400);
}
// Load manifest data
$manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php');
if (!file_exists($manifest_file)) {
error_response('Manifest not found - run php artisan rsx:manifest:build', 500);
}
$manifest_raw = include $manifest_file;
$manifest_data = $manifest_raw['data'] ?? $manifest_raw;
$files = $manifest_data['files'] ?? [];
// Find the JavaScript class and trace its lineage
$lineage = [];
$current_class = $class_name;
// Helper to find extends clause in JS file
$find_extends = function($file_path) {
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
if (preg_match('/class\s+\w+\s+extends\s+([A-Za-z_][A-Za-z0-9_]*)/', $content, $matches)) {
return $matches[1];
}
}
return null;
};
// Trace up to 10 levels to prevent infinite loops
for ($i = 0; $i < 10; $i++) {
$found = false;
foreach ($files as $file_path => $file_data) {
if (str_ends_with($file_path, '.js')) {
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
// Check if this file defines the current class
if (preg_match('/class\s+' . preg_quote($current_class, '/') . '\s+extends\s+([A-Za-z_][A-Za-z0-9_]*)/', $content, $matches)) {
$parent_class = $matches[1];
$lineage[] = $parent_class;
$current_class = $parent_class;
$found = true;
break;
}
}
}
}
if (!$found) {
break;
}
}
json_response([
'class' => $class_name,
'lineage' => $lineage,
]);
}
/**
* Handle resolve_url service - resolve URL to controller/method
* Takes a URL path and returns the controller and method that handles it
*/
function handle_resolve_url_service($data) {
$url = $data['url'] ?? null;
if (!$url) {
json_response([
'error' => 'Missing required parameter: url',
'found' => false,
], 400);
}
// Load manifest to get routes
$manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php');
if (!file_exists($manifest_file)) {
error_response('Manifest not found - run php artisan rsx:manifest:build', 500);
}
$manifest_raw = include $manifest_file;
$manifest_data = $manifest_raw['data'] ?? $manifest_raw;
// Get routes from manifest
$routes = $manifest_data['php']['routes'] ?? [];
// Try to find matching route
foreach ($routes as $route) {
$pattern = $route['pattern'] ?? '';
$controller = $route['class'] ?? '';
$method = $route['method'] ?? '';
// Simple pattern matching - exact match for now
// TODO: Support route parameters like /users/:id
if ($pattern === $url) {
json_response([
'found' => true,
'controller' => $controller,
'method' => $method,
'pattern' => $pattern,
]);
}
}
// Not found
json_response([
'found' => false,
'url' => $url,
]);
}