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