Files
rspade_system/app/RSpade/Ide/Services/handler.php
root 9ebcc359ae Fix code quality violations and enhance ROUTE-EXISTS-01 rule
Implement JQHTML function cache ID system and fix bundle compilation
Implement underscore prefix for system tables
Fix JS syntax linter to support decorators and grant exception to Task system
SPA: Update planning docs and wishlists with remaining features
SPA: Document Navigation API abandonment and future enhancements
Implement SPA browser integration with History API (Phase 1)
Convert contacts view page to SPA action
Convert clients pages to SPA actions and document conversion procedure
SPA: Merge GET parameters and update documentation
Implement SPA route URL generation in JavaScript and PHP
Implement SPA bootstrap controller architecture
Add SPA routing manual page (rsx:man spa)
Add SPA routing documentation to CLAUDE.md
Phase 4 Complete: Client-side SPA routing implementation
Update get_routes() consumers for unified route structure
Complete SPA Phase 3: PHP-side route type detection and is_spa flag
Restore unified routes structure and Manifest_Query class
Refactor route indexing and add SPA infrastructure
Phase 3 Complete: SPA route registration in manifest
Implement SPA Phase 2: Extract router code and test decorators
Rename Jqhtml_Component to Component and complete SPA foundation setup

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 17:48:15 +00:00

1602 lines
52 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
*
* Localhost Bypass:
* Authentication can be bypassed when ALL of the following conditions are met:
* 1. Request is to hostname 'localhost' (not 127.0.0.1, not any other domain)
* 2. Request is NOT HTTPS (must be http://)
* 3. Request comes from a loopback IP address (127.x.x.x or ::1)
* 4. No X-* proxy headers present (except X-Session/X-Signature)
* 5. Laravel is NOT in production mode
*
* This allows the VS Code extension to make unauthenticated requests when running
* on the same machine as the dev server, eliminating session management overhead
* for local development while maintaining security for remote connections.
*/
// 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');
// Helper to normalize file paths for IDE consumption
// Framework files (not starting with rsx/) need system/ prefix
function normalize_ide_path($path) {
if (!$path || str_starts_with($path, 'rsx/')) {
return $path;
}
return 'system/' . $path;
}
// Helper to recursively normalize all 'file' keys in response data
function normalize_response_paths($data) {
if (is_array($data)) {
foreach ($data as $key => $value) {
if ($key === 'file' && is_string($value)) {
$data[$key] = normalize_ide_path($value);
} elseif (is_array($value)) {
$data[$key] = normalize_response_paths($value);
}
}
}
return $data;
}
// JSON response helper
function json_response($data, $code = 200) {
// Normalize all file paths in response for IDE consumption
$data = normalize_response_paths($data);
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);
}
// Authentication already handled by auth.php before this file is loaded
// Retrieve auth data from constants set by auth.php
if (!defined('IDE_AUTH_PASSED')) {
error_response('Authentication check did not run - this should never happen', 500);
}
$auth_data = json_decode(IDE_AUTH_DATA, true);
// 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);
}
// Route to appropriate service handler
// This handler handles lightweight services that don't need Laravel bootstrap
// All services requiring Manifest/Laravel are handled by laravel_handler.php
// Note: auth/create is handled in auth.php before routing
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':
case 'command':
handle_exec_service($request_data);
break;
case 'resolve_class':
handle_resolve_class_service($request_data);
break;
case 'git':
handle_git_service($request_data);
break;
case 'git/diff':
handle_git_diff_service($request_data);
break;
case 'health':
json_response(['success' => true, 'service' => 'ide-standalone', 'version' => '1.0.0']);
break;
default:
error_response('Unknown service: ' . $service_path . ' (standalone handler supports: format, definition, complete, exec, command, resolve_class, git, git/diff)', 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 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
]);
}
/**
* Try to resolve identifier as a PHP class
*/
function try_resolve_php_class($identifier, $method_name, $find_php_class) {
$class_data = $find_php_class($identifier);
if (!$class_data) {
return null;
}
$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' => 'php_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;
}
return $response_data;
}
/**
* Try to resolve identifier as a JavaScript class
*/
function try_resolve_js_class($identifier, $method_name, $files) {
foreach ($files as $file_path => $file_data) {
// Only check .js files (not .jqhtml)
if (str_ends_with($file_path, '.js') && !str_ends_with($file_path, '.jqhtml')) {
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
// Check for JavaScript class (not specifically Component)
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends\s+([A-Za-z_][A-Za-z0-9_]*)/', $content, $matches)) {
$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;
}
}
return [
'found' => true,
'type' => 'js_class',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
'extends' => $matches[1] ?? null,
];
}
}
}
}
return null;
}
/**
* Try to resolve identifier as a jqhtml component class
*
* Note: This resolver does NOT check inheritance chains. It's a simple file lookup.
* VS Code extension should use js_is_subclass_of() RPC to determine if a class
* is actually a Component before choosing this resolver type.
*/
function try_resolve_jqhtml_class($identifier, $method_name, $files) {
// Load manifest to get js_classes index
$manifest_file = ide_framework_path('storage/rsx-build/manifest_data.php');
if (!file_exists($manifest_file)) {
return null;
}
$manifest_raw = include $manifest_file;
$manifest_data = $manifest_raw['data'] ?? $manifest_raw;
$js_classes = $manifest_data['js_classes'] ?? [];
// Check if this class exists in js_classes
if (!isset($js_classes[$identifier])) {
return null;
}
// Get the file path
$file_path = $js_classes[$identifier];
// Find the line number
$absolute_path = IDE_BASE_PATH . '/' . $file_path;
$line_number = 1;
if (file_exists($absolute_path)) {
$content = file_get_contents($absolute_path);
$lines = explode("\n", $content);
foreach ($lines as $index => $line) {
if (preg_match('/class\s+' . preg_quote($identifier, '/') . '\s+extends/', $line)) {
$line_number = $index + 1;
break;
}
}
}
return [
'found' => true,
'type' => 'jqhtml_class',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
];
}
/**
* Try to resolve identifier as a view
*/
function try_resolve_view($identifier, $find_view) {
$view_data = $find_view($identifier);
if (!$view_data) {
return null;
}
$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;
}
}
}
return [
'found' => true,
'type' => 'view',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
];
}
/**
* Try to resolve identifier as a bundle alias
*/
function try_resolve_bundle_alias($identifier, $find_php_class) {
$config_path = IDE_SYSTEM_PATH . '/config/rsx.php';
if (!file_exists($config_path)) {
return null;
}
$config = include $config_path;
if (!isset($config['bundle_aliases'][$identifier])) {
return null;
}
$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) {
return null;
}
$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;
}
}
}
return [
'found' => true,
'type' => 'bundle_alias',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
'resolved_class' => $bundle_class,
];
}
/**
* Try to resolve identifier as a jqhtml template
*/
function try_resolve_jqhtml_template($identifier, $files, $camel_to_snake, $snake_to_pascal) {
$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;
}
}
}
return [
'found' => true,
'type' => 'jqhtml_template',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
];
}
}
}
return null;
}
/**
* Try to resolve method in jqhtml component
*/
function try_resolve_jqhtml_method($identifier, $method_name, $files) {
$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_]*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;
}
}
}
return [
'found' => true,
'type' => 'jqhtml_class_method',
'file' => $file_path,
'line' => $line_number,
'identifier' => $identifier,
'method' => $method_name,
];
}
}
}
}
return null;
}
/**
* Handle resolve_class service - resolve class/view/component definitions
*
* RESOLUTION TYPE SYSTEM
* ======================
*
* This endpoint resolves identifiers to file locations based on type priority.
* It supports CSV type lists for priority-based resolution.
*
* AVAILABLE TYPES:
*
* 1. php_class - PHP class files (*.php)
* - Searches manifest for PHP classes
* - Supports method lookup with fallback to class
* - Returns: file path, line number, namespace, extends, fqcn
*
* 2. js_class - JavaScript class files (*.js, not .jqhtml)
* - Searches for JS classes with extends clause
* - Does NOT require extending Component
* - Returns: file path, line number, extends
*
* 3. jqhtml_class - jqhtml component JavaScript files (*.js)
* - Searches for classes extending Component
* - Returns: file path, line number
*
* 4. view - Blade view templates (*.blade.php)
* - Searches for @rsx_id directives
* - Returns: file path, line number
*
* 5. bundle_alias - Bundle configuration aliases
* - Resolves bundle aliases from config/rsx.php
* - Returns: bundle class file location
*
* 6. jqhtml_template - jqhtml template files (*.jqhtml)
* - Searches for <Define:ComponentName> tags
* - Returns: template file path, line number
*
* 7. jqhtml_class_method - Methods in jqhtml component classes
* - Requires both identifier and method parameters
* - Searches within Component subclasses
* - Returns: file path, method line number
*
* CSV TYPE LISTS:
* ===============
*
* The type parameter accepts comma-separated lists for priority-based resolution.
* The server tries each type in order and returns the first match found.
*
* Examples:
* - type=php_class,js_class
* Try PHP classes first, then JavaScript classes
* Use case: From PHP files (always want PHP), fallback to JS
*
* - type=js_class,php_class
* Try JavaScript classes first, then PHP classes
* Use case: From JS files (component inheritance), fallback to PHP controllers
*
* - type=jqhtml_class,js_class
* Try jqhtml components first, then general JS classes
* Use case: jqhtml extends="" attribute resolution
*
* METHOD RESOLUTION:
* ==================
*
* When the method parameter is provided, the resolver attempts to find the specific
* method within the class. If the method exists, returns its line number. If the
* method doesn't exist but the class does, returns the class line number as fallback.
*
* This allows graceful degradation: clicking on Controller.missing_method() will
* navigate to the Controller class even if missing_method() doesn't exist.
*
* USAGE FROM VS CODE:
* ===================
*
* The VS Code extension determines which types to use based on context:
*
* - Route patterns → php_class (routes are always PHP)
* - PHP class references → php_class (PHP files only reference PHP)
* - JS class references → js_class,php_class (try JS first, then PHP controllers)
* - jqhtml extends="" → jqhtml_class,js_class (component inheritance)
* - jqhtml $xxx=Class.method → js_class,php_class (components or controllers)
* - jqhtml $xxx=this.method → jqhtml_class_method (current component only)
*
* PARAMETERS:
* - identifier/class (required): The class/view/component name to resolve
* - method (optional): Method name within the class
* - type (optional): Single type or CSV list of types to try in order
*/
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);
}
// Support CSV type list for priority ordering
$type_list = [];
if ($type) {
$type_list = array_map('trim', explode(',', $type));
}
// 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) {
// Only match PHP files - check file extension and presence of PHP-specific metadata
if (isset($file_data['class']) && $file_data['class'] === $class_name) {
// Must be a PHP file (not .js, not .jqhtml)
if (str_ends_with($file_path, '.php')) {
// Add file path to the data so caller knows which file it came from
$file_data['file'] = $file_path;
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));
};
// If no type list specified, try auto-detection with legacy 'class' type
if (empty($type_list)) {
if (!$type || $type === 'class' || preg_match('/^[A-Z][A-Za-z0-9_]*$/', $identifier)) {
// Legacy: Try PHP class with auto-detection fallback
$type_list = ['php_class', 'view', 'bundle_alias', 'jqhtml_template', 'jqhtml_class'];
}
}
// Try each type in priority order
foreach ($type_list as $current_type) {
$result = null;
switch ($current_type) {
case 'php_class':
$result = try_resolve_php_class($identifier, $method_name, $find_php_class);
break;
case 'js_class':
$result = try_resolve_js_class($identifier, $method_name, $files);
break;
case 'jqhtml_class':
$result = try_resolve_jqhtml_class($identifier, $method_name, $files);
break;
case 'view':
$result = try_resolve_view($identifier, $find_view);
break;
case 'bundle_alias':
$result = try_resolve_bundle_alias($identifier, $find_php_class);
break;
case 'jqhtml_template':
$result = try_resolve_jqhtml_template($identifier, $files, $camel_to_snake, $snake_to_pascal);
break;
case 'jqhtml_class_method':
if ($method_name) {
$result = try_resolve_jqhtml_method($identifier, $method_name, $files);
}
break;
}
// If we found a result, return it immediately
if ($result && ($result['found'] ?? false)) {
json_response($result);
}
}
// Nothing found after trying all types
json_response([
'found' => false,
'error' => 'Identifier not found in manifest',
'identifier' => $identifier,
'searched_types' => $type_list,
]);
}
/**
* OBSOLETE LEGACY CODE REMOVED
* =============================
* The code below (approximately lines 1410-1685) was the old resolution system
* that tried each type individually without CSV support. It has been replaced
* by the new CSV type list system above with dedicated resolver functions.
*
* Removed sections:
* - Legacy PHP class fallback (duplicate of try_resolve_php_class)
* - Legacy view resolution (duplicate of try_resolve_view)
* - Legacy bundle alias resolution (duplicate of try_resolve_bundle_alias)
* - Legacy jqhtml template resolution (duplicate of try_resolve_jqhtml_template)
* - Legacy jqhtml class resolution (duplicate of try_resolve_jqhtml_class)
* - Legacy jqhtml method resolution (duplicate of try_resolve_jqhtml_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 js_is_subclass_of service - check if JS class extends another
*
* This is a direct RPC wrapper around Manifest::js_is_subclass_of()
*
* PARAMETERS:
* - subclass (required): The potential subclass name
* - superclass (required): The potential superclass name
*
* RETURNS:
* - is_subclass: boolean - true if subclass extends superclass anywhere in chain
*/
function handle_js_is_subclass_of_service($data) {
$subclass = $data['subclass'] ?? null;
$superclass = $data['superclass'] ?? null;
if (!$subclass || !$superclass) {
json_response([
'error' => 'Missing required parameters: subclass and superclass',
], 400);
}
// Load manifest
$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;
$js_classes = $manifest_data['js_classes'] ?? [];
$files = $manifest_data['files'] ?? [];
// Implement same logic as Manifest::js_is_subclass_of
$current_class = $subclass;
$visited = [];
while ($current_class) {
// Prevent infinite loops
if (in_array($current_class, $visited)) {
json_response(['is_subclass' => false]);
}
$visited[] = $current_class;
// Find the current class in the manifest
if (!isset($js_classes[$current_class])) {
json_response(['is_subclass' => false]);
}
// Get file metadata
$file_path = $js_classes[$current_class];
$metadata = $files[$file_path] ?? null;
if (!$metadata || empty($metadata['extends'])) {
json_response(['is_subclass' => false]);
}
if ($metadata['extends'] == $superclass) {
json_response(['is_subclass' => true]);
}
// Move up the chain to the parent class
$current_class = $metadata['extends'];
}
json_response(['is_subclass' => false]);
}
/**
* Handle php_is_subclass_of service - check if PHP class extends another
*
* This is a direct RPC wrapper around Manifest::php_is_subclass_of()
*
* PARAMETERS:
* - subclass (required): The potential subclass name
* - superclass (required): The potential superclass name
*
* RETURNS:
* - is_subclass: boolean - true if subclass extends superclass anywhere in chain
*/
function handle_php_is_subclass_of_service($data) {
$subclass = $data['subclass'] ?? null;
$superclass = $data['superclass'] ?? null;
if (!$subclass || !$superclass) {
json_response([
'error' => 'Missing required parameters: subclass and superclass',
], 400);
}
// Load manifest
$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'] ?? [];
// Implement same logic as Manifest::php_is_subclass_of
$current_class = $subclass;
$visited = [];
while ($current_class) {
// Prevent infinite loops
if (in_array($current_class, $visited)) {
json_response(['is_subclass' => false]);
}
$visited[] = $current_class;
// Find the current class in files
$found = false;
foreach ($files as $file_path => $file_data) {
if (isset($file_data['class']) && $file_data['class'] === $current_class) {
// Must be a PHP file
if (str_ends_with($file_path, '.php')) {
$extends = $file_data['extends'] ?? null;
if (!$extends) {
json_response(['is_subclass' => false]);
}
// Normalize the extends class name (strip namespace)
$extends_parts = explode('\\', $extends);
$extends_simple = end($extends_parts);
if ($extends_simple == $superclass) {
json_response(['is_subclass' => true]);
}
// Move up the chain
$current_class = $extends_simple;
$found = true;
break;
}
}
}
if (!$found) {
json_response(['is_subclass' => false]);
}
}
json_response(['is_subclass' => false]);
}
/**
* 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,
]);
}