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

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

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

451 lines
17 KiB
PHP
Executable File

<?php
/**
* CODING CONVENTION:
* This file follows the coding convention where variable_names and function_names
* use snake_case (underscore_wherever_possible).
*/
namespace App\RSpade\Core\Ajax;
use Exception;
use Illuminate\Http\Request;
use App\RSpade\Core\Ajax\Exceptions\AjaxAuthRequiredException;
use App\RSpade\Core\Ajax\Exceptions\AjaxFatalErrorException;
use App\RSpade\Core\Ajax\Exceptions\AjaxFormErrorException;
use App\RSpade\Core\Ajax\Exceptions\AjaxUnauthorizedException;
use App\RSpade\Core\Manifest\Manifest;
use App\RSpade\Core\Response\Rsx_Response_Abstract;
// @FILE-SUBCLASS-01-EXCEPTION
/**
* Ajax - Unified AJAX handling system
*
* Consolidates all AJAX-related functionality:
* - Internal server-side API calls (internal method)
* - Browser AJAX request handling (handle_browser_request method)
* - Response mode management for error handlers
* - Future: External HTTP calls, WebSocket handling
*/
class Ajax
{
/**
* Flag to indicate AJAX response mode for error handlers
*/
protected static bool $ajax_response_mode = false;
/**
* Call an internal API method directly from PHP code
*
* Used for server-side code to invoke API methods without HTTP overhead.
* This is useful for internal service calls, background jobs, and testing.
*
* @param string $rsx_controller Controller name (e.g., 'User_Controller')
* @param string $rsx_action Action/method name (e.g., 'get_profile')
* @param array $params Parameters to pass to the method
* @param array $auth Authentication context (not yet implemented)
* @return mixed The filtered response from the API method
* @throws AjaxAuthRequiredException
* @throws AjaxUnauthorizedException
* @throws AjaxFormErrorException
* @throws AjaxFatalErrorException
* @throws Exception
*/
public static function internal($rsx_controller, $rsx_action, $params = [], $auth = [])
{
// Get manifest to find controller
$manifest = \App\RSpade\Core\Manifest\Manifest::get_all();
$controller_class = null;
$file_info = null;
// Search for controller class in manifest
foreach ($manifest as $file_path => $info) {
// Skip non-PHP files or files without classes
if (!isset($info['class']) || !isset($info['fqcn'])) {
continue;
}
// Check if class name matches exactly (without namespace)
$class_basename = basename(str_replace('\\', '/', $info['fqcn']));
if ($class_basename === $rsx_controller) {
$controller_class = $info['fqcn'];
$file_info = $info;
break;
}
}
if (!$controller_class) {
throw new Exception("Controller class not found: {$rsx_controller}");
}
// Check if class exists
if (!class_exists($controller_class)) {
throw new Exception("Controller class does not exist: {$controller_class}");
}
// Check if it's a subclass of Rsx_Controller_Abstract
if (!Manifest::php_is_subclass_of($controller_class, \App\RSpade\Core\Controller\Rsx_Controller_Abstract::class)) {
throw new Exception("Controller {$controller_class} must extend Rsx_Controller_Abstract");
}
// Check if method exists and has Ajax_Endpoint attribute
if (!isset($file_info['public_static_methods'][$rsx_action])) {
throw new Exception("Method {$rsx_action} not found in controller {$controller_class}");
}
$method_info = $file_info['public_static_methods'][$rsx_action];
$has_ajax_endpoint = false;
// Check for Ajax_Endpoint attribute in method metadata
if (isset($method_info['attributes'])) {
foreach ($method_info['attributes'] as $attr_name => $attr_data) {
// Check for Ajax_Endpoint with or without namespace
if ($attr_name === 'Ajax_Endpoint' ||
basename(str_replace('\\', '/', $attr_name)) === 'Ajax_Endpoint') {
$has_ajax_endpoint = true;
break;
}
}
}
if (!$has_ajax_endpoint) {
throw new Exception("Method {$rsx_action} in {$controller_class} must have Ajax_Endpoint annotation");
}
// Create a request object with the params
$request = Request::create('/_ajax/' . $rsx_controller . '/' . $rsx_action, 'POST', $params);
$request->setMethod('POST');
// Call pre_dispatch if it exists
$response = null;
if (method_exists($controller_class, 'pre_dispatch')) {
$response = $controller_class::pre_dispatch($request, $params);
}
// If pre_dispatch returned something, use that as response
if ($response === null) {
// Call the actual method
$response = $controller_class::$rsx_action($request, $params);
}
// Handle special response types
if ($response instanceof Rsx_Response_Abstract) {
return static::_handle_special_response($response);
}
// For normal responses, filter through JSON encode/decode to remove PHP objects
// This ensures we only return plain data structures
$filtered_response = json_decode(json_encode($response), true);
return $filtered_response;
}
/**
* Handle special response types by throwing appropriate exceptions
*
* @param Rsx_Response_Abstract $response
* @throws AjaxAuthRequiredException
* @throws AjaxUnauthorizedException
* @throws AjaxFormErrorException
* @throws AjaxFatalErrorException
*/
protected static function _handle_special_response(Rsx_Response_Abstract $response)
{
$type = $response->get_type();
$reason = $response->get_reason();
$details = $response->get_details();
switch ($type) {
case 'response_auth_required':
throw new AjaxAuthRequiredException($reason);
case 'response_unauthorized':
throw new AjaxUnauthorizedException($reason);
case 'response_form_error':
throw new AjaxFormErrorException($reason, $details);
case 'fatal':
$message = $reason;
if (!empty($details)) {
$message .= ' - ' . json_encode($details);
}
throw new AjaxFatalErrorException($message);
default:
throw new Exception("Unknown RSX response type: {$type}");
}
}
/**
* Handle incoming AJAX requests from the browser
*
* This method processes /_ajax/:controller/:action requests and returns JSON responses.
* It validates Ajax_Endpoint annotations and handles special response types.
*
* @param Request $request The incoming HTTP request
* @param array $params Route parameters including controller and action
* @return \Illuminate\Http\JsonResponse
* @throws Exception
*/
public static function handle_browser_request(Request $request, array $params = [])
{
// Enable AJAX response mode for error handlers
static::set_ajax_response_mode(true);
// Disable console debug HTML output for AJAX requests
\App\RSpade\Core\Debug\Debugger::disable_console_html_output();
// Get controller and action from params
$controller_name = $params['controller'] ?? null;
$action_name = $params['action'] ?? null;
if (!$controller_name || !$action_name) {
throw new Exception('Missing controller or action parameter');
}
// Use manifest to find the controller class
$manifest = \App\RSpade\Core\Manifest\Manifest::get_all();
$controller_class = null;
$file_info = null;
// Search for controller class in manifest
foreach ($manifest as $file_path => $info) {
// Skip non-PHP files or files without classes
if (!isset($info['class']) || !isset($info['fqcn'])) {
continue;
}
// Check if class name matches exactly (without namespace)
$class_basename = basename(str_replace('\\', '/', $info['fqcn']));
if ($class_basename === $controller_name) {
$controller_class = $info['fqcn'];
$file_info = $info;
break;
}
}
if (!$controller_class) {
throw new Exception("Controller class not found: {$controller_name}");
}
// Check if class exists
if (!class_exists($controller_class)) {
throw new Exception("Controller class does not exist: {$controller_class}");
}
// Check if it's a subclass of Rsx_Controller_Abstract
if (!Manifest::php_is_subclass_of($controller_class, \App\RSpade\Core\Controller\Rsx_Controller_Abstract::class)) {
throw new Exception("Controller {$controller_class} must extend Rsx_Controller_Abstract");
}
// Check if method exists and has Ajax_Endpoint attribute
if (!isset($file_info['public_static_methods'][$action_name])) {
throw new Exception("Method {$action_name} not found in controller {$controller_class} public static methods");
}
$method_info = $file_info['public_static_methods'][$action_name];
$has_ajax_endpoint = false;
// Check for Ajax_Endpoint attribute in method metadata
if (isset($method_info['attributes'])) {
foreach ($method_info['attributes'] as $attr_name => $attr_data) {
// Check for Ajax_Endpoint with or without namespace
if ($attr_name === 'Ajax_Endpoint' ||
basename(str_replace('\\', '/', $attr_name)) === 'Ajax_Endpoint') {
$has_ajax_endpoint = true;
break;
}
}
}
if (!$has_ajax_endpoint) {
throw new Exception("Method {$action_name} in {$controller_class} must have Ajax_Endpoint annotation");
}
// Extract params from request body
$ajax_params = [];
// First check for JSON-encoded params field (old format)
$params_json = $request->input('params');
if ($params_json) {
$ajax_params = json_decode($params_json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Invalid JSON in params: ' . json_last_error_msg());
}
} else {
// Get all request input (form-urlencoded or JSON body)
$all_input = $request->all();
// Remove route parameters (controller, action, etc)
$ajax_params = array_diff_key($all_input, array_flip(['controller', 'action', '_method', '_route', '_handler']));
}
// Call pre_dispatch if it exists
$response = null;
if (method_exists($controller_class, 'pre_dispatch')) {
$response = $controller_class::pre_dispatch($request, $ajax_params);
}
// If pre_dispatch returned something, use that as response
if ($response === null) {
// Call the actual method
$response = $controller_class::$action_name($request, $ajax_params);
}
// Handle special response types
if ($response instanceof \App\RSpade\Core\Response\Rsx_Response_Abstract) {
return static::_handle_browser_special_response($response);
}
// Wrap response using shared formatting method
$json_response = static::format_ajax_response($response);
return response()->json($json_response);
}
/**
* Handle special response types for browser AJAX requests
*
* @param \App\RSpade\Core\Response\Rsx_Response_Abstract $response
* @return \Illuminate\Http\JsonResponse
*/
protected static function _handle_browser_special_response(\App\RSpade\Core\Response\Rsx_Response_Abstract $response)
{
$type = $response->get_type();
// Handle fatal error - always throw exception
if ($type === 'fatal') {
$details = $response->get_details();
$message = $response->get_reason();
if (!empty($details)) {
$message .= ' - ' . json_encode($details);
}
throw new Exception($message);
}
// Build error response based on type
$json_response = [
'_success' => false,
'error_type' => $type,
'reason' => $response->get_reason(),
];
// Add details for form errors
if ($type === 'response_form_error') {
$json_response['details'] = $response->get_details();
}
// Add console debug messages if any
$console_messages = \App\RSpade\Core\Debug\Debugger::_get_console_messages();
if (!empty($console_messages)) {
// Messages are now structured as [channel, [arguments]]
$json_response['console_debug'] = $console_messages;
}
return response()->json($json_response);
}
/**
* Set AJAX response mode for error handlers
*
* When enabled, error handlers will return JSON instead of HTML
*
* @param bool $enabled
* @return void
*/
public static function set_ajax_response_mode(bool $enabled): void
{
static::$ajax_response_mode = $enabled;
}
/**
* Check if AJAX response mode is enabled
*
* @return bool
*/
public static function is_ajax_response_mode(): bool
{
return static::$ajax_response_mode;
}
/**
* Validate Ajax endpoint return value
*
* Ensures developers don't manually add 'success' field to their responses.
* The framework automatically wraps responses with success/failure metadata.
*
* @param mixed $response The Ajax method return value
* @throws Exception If response contains manual success field
*/
protected static function _validate_ajax_response($response): void
{
// Only validate if response is an array
if (!is_array($response)) {
return;
}
// Check if response contains '_success' or 'success' key with boolean value
if ((array_key_exists('_success', $response) && is_bool($response['_success'])) ||
(array_key_exists('success', $response) && is_bool($response['success']))) {
$wrong_way = "return ['_success' => false, 'message' => 'Error'];";
$right_way_validation = "return response_form_error('Validation failed', ['email' => 'Invalid']);";
$right_way_success = "return ['user_id' => 123, 'data' => [...]];\n// Framework wraps: {_success: true, _ajax_return_value: {...}}";
$right_way_exception = "// Let exceptions bubble - framework handles them\n\$user->save(); // Don't wrap in try/catch";
throw new Exception(
"YOU'RE DOING IT WRONG: Ajax endpoints must not return arrays with '_success' or 'success' boolean key.\n\n" .
"The '_success' field is reserved for framework-level Ajax response wrapping.\n\n" .
"WRONG WAY:\n" .
" {$wrong_way}\n\n" .
"RIGHT WAY (validation errors):\n" .
" {$right_way_validation}\n\n" .
"RIGHT WAY (success):\n" .
" {$right_way_success}\n\n" .
"RIGHT WAY (exceptions):\n" .
" {$right_way_exception}\n\n" .
"See: php artisan rsx:man ajax_error_handling"
);
}
}
/**
* Format Ajax response with standard wrapper structure
*
* This method creates the standardized response format used by both
* HTTP Ajax endpoints and CLI Ajax commands. Ensures consistency across
* all Ajax response types.
*
* @param mixed $response The Ajax method return value
* @return array Formatted response array with success, _ajax_return_value, and optional console_debug
*/
public static function format_ajax_response($response): array
{
// Validate response before formatting
static::_validate_ajax_response($response);
$json_response = [
'_success' => true,
'_ajax_return_value' => $response,
];
// Add console debug messages if any
$console_messages = \App\RSpade\Core\Debug\Debugger::_get_console_messages();
if (!empty($console_messages)) {
// Messages are now structured as [channel, [arguments]]
$json_response['console_debug'] = $console_messages;
}
return $json_response;
}
/**
* Backward compatibility alias for internal()
* @deprecated Use internal() instead
*/
public static function call($rsx_controller, $rsx_action, $params = [], $auth = [])
{
return static::internal($rsx_controller, $rsx_action, $params, $auth);
}
}