Fix bin/publish: use correct .env path for rspade_system Fix bin/publish script: prevent grep exit code 1 from terminating script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
373 lines
13 KiB
PHP
Executable File
373 lines
13 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 'response_fatal_error':
|
|
$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");
|
|
}
|
|
|
|
// 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::$action_name($request, $params);
|
|
}
|
|
|
|
// Handle special response types
|
|
if ($response instanceof \App\RSpade\Core\Response\Rsx_Response_Abstract) {
|
|
return static::_handle_browser_special_response($response);
|
|
}
|
|
|
|
// Wrap normal response in success JSON
|
|
$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 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 === 'response_fatal_error') {
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|