diff --git a/docs/CodeHydrater.md b/docs/CodeHydrater.md index 72e735b..8b14088 100644 --- a/docs/CodeHydrater.md +++ b/docs/CodeHydrater.md @@ -96,6 +96,7 @@ src/ │   │   ├── kernel.php │   │   ├── request.php │   │   ├── response.php +│   │   ├── route_service_provider │   │   └── service_provider.php │   ├── interfaces │   ├── lazy_collection.php diff --git a/docs/TODO.md b/docs/TODO.md index 0148e4d..43bde7d 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -5,7 +5,7 @@ - [x] → Encrypted/Compressed Sessions - [ ] → Ensure JS error reporting works - [ ] → Force Database methods to try Cache First and Save Cache Data -- [ ] → Kernels (exists but not used yet) +- [x] → Kernels - [x] → HTTP → Requests, Responce - [x] → Middleware - [x] → Macros diff --git a/src/classes/base_controller.php b/src/classes/base_controller.php new file mode 100644 index 0000000..c6d2233 --- /dev/null +++ b/src/classes/base_controller.php @@ -0,0 +1,40 @@ + + * @copyright (c) 2025, Robert Strutts + * @license MIT + */ +namespace CodeHydrater; + +use \CodeHydrater\http\request as Request; +use \CodeHydrater\http\response as Response; + +/** + * Description of base_controller + * + * To setup child controllers to use request/response, and view/html. + * + * @author Robert Strutts + */ +abstract class base_controller { + + protected $view; + protected $html; + + public function __construct( + protected Request $request, + protected Response $response + ) { + $this->view = new \CodeHydrater\view(); + $this->html = new \CodeHydrater\html_document(); + + // If the child controller has an init() method, call it automatically + if (method_exists($this, 'init')) { + $this->init(); + } + } + +} diff --git a/src/classes/common.php b/src/classes/common.php index 287c010..b1749a2 100644 --- a/src/classes/common.php +++ b/src/classes/common.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace CodeHydrater; use CodeHydrater\strings\string_facade as F; +use CodeHydrater\enums\exit_on_dump as endDump; final class common { @@ -214,7 +215,10 @@ final class common { * @param var - any type will display type and value of contents * @param bool end - if true ends the script */ - public static function dump($var = 'nothing', $end = true): void { + public static function dump( + $var = 'nothing', + endDump $end = endDump::exit_and_stop + ): void { if (\CodeHydrater\bootstrap\configure::get('security', 'show_dumps') !== true) { return; } diff --git a/src/classes/enums/exit_on_dump.php b/src/classes/enums/exit_on_dump.php new file mode 100644 index 0000000..9ea35bd --- /dev/null +++ b/src/classes/enums/exit_on_dump.php @@ -0,0 +1,15 @@ + + * @copyright (c) 2025, Robert Strutts + * @license MIT + */ +namespace CodeHydrater\enums; + +enum exit_on_dump: int { + case exit_and_stop = 0; + case keep_working = 1; +} \ No newline at end of file diff --git a/src/classes/http/request.php b/src/classes/http/request.php index 8452236..7b6adb0 100644 --- a/src/classes/http/request.php +++ b/src/classes/http/request.php @@ -9,32 +9,38 @@ declare(strict_types = 1); */ namespace CodeHydrater\http; -class request -{ - protected array $query_params; - protected array $post_data; - protected array $server; - protected array $cookies; - protected array $files; - protected array $headers; +use CodeHydrater\parameter_bag; + +class request { + use \CodeHydrater\traits\Macroable; + + protected $query_params = []; + protected $post_data = []; + protected $server = []; + protected $cookies = []; + protected $files = []; + protected $headers = []; public function __construct( - array $query_params = [], - array $post_data = [], - array $server = [], - array $cookies = [], - array $files = [], - array $headers = [] + $query_params = [], + $post_data = [], + $server = [], + $cookies = [], + $files = [], + $headers = [] ) { - $this->query_params = $query_params; - $this->post_data = $post_data; - $this->server = $server; - $this->cookies = $cookies; - $this->files = $files; - $this->headers = $headers; + $this->query_params = new parameter_bag($query_params); + $this->post_data = new parameter_bag($post_data); + $this->server = new parameter_bag($server); + $this->cookies = new parameter_bag($cookies); + $this->files = new parameter_bag($files); + $this->headers = new parameter_bag($headers); } public static function create_from_globals(): self { + if (\CodeHydrater\console_app::is_cli()) { + return new self(); + } return new self( $_GET, $_POST, @@ -46,22 +52,122 @@ class request } public function get_method(): string { - return strtoupper($this->server['REQUEST_METHOD'] ?? 'GET'); + return strtoupper($this->server->get('REQUEST_METHOD') ?? 'GET'); } public function get_uri(): string { - return $this->server['REQUEST_URI'] ?? '/'; + return rtrim(preg_replace('/\?.*/', '', $this->server->get('REQUEST_URI')), '/') ?? '/'; } - public function get_query_params(): array { + public function get_query_params(): parameter_bag { return $this->query_params; } - public function get_post_data(): array { + public function get_post_data(): parameter_bag { return $this->post_data; } + public function get_server_data(): parameter_bag { + return $this->server; + } + + public function get_cookie_data(): parameter_bag { + return $this->cookies; + } + + public function get_files(): parameter_bag { + return $this->files; + } + + public function get_headers(): parameter_bag { + return $this->headers; + } + public function get_header(string $name): ?string { return $this->headers[$name] ?? null; } + + public function ajax(): bool { + return $this->is_xml_http_request(); + } + + /** + * Determine if the request is the result of an AJAX call. + * Checks for the 'X-Requested-With' header, which is commonly sent by JavaScript libraries. + */ + public function is_xml_http_request(): bool { + // The header name is standardized, but we normalize it for case-insensitive comparison. + $header = $this->headers->get('X-Requested-With'); + + // Check if the header exists and its value is 'XMLHttpRequest' + return 'XMLHttpRequest' == $header; + } + + /** + * Get the client's real IP address or Null if not known. + * This method is aware of common proxy headers that might contain the original client IP. + */ + public function ip(): ?string { + // Define a list of headers that proxies might use to forward the original IP. + // The order is important: we trust the left-most IP in the right-most header. + $proxyHeaders = [ + 'X-Forwarded-For', + 'X-Real-IP', + 'Client-IP', // Less common, but used sometimes + ]; + + // First, check the proxy headers. We iterate in reverse order of trustworthiness. + // The last header in the array is considered the most trustworthy (closest to the client). + foreach (array_reverse($proxyHeaders) as $header) { + if ($this->headers->has($header)) { + // Headers like 'X-Forwarded-For' can contain a comma-separated list of IPs. + // The client's original IP is usually the first one in the list. + $ips = explode(',', $this->headers->get($header)); + // Get the first IP from the list and trim any whitespace. + $clientIp = trim($ips[0]); + + // Basic validation to ensure it's a plausible IP address. + if (filter_var($clientIp, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { + return $clientIp; + } + } + } + + // If no valid proxy header is found, fall back to the direct connection IP. + // This will be the IP of the last proxy talking to our server (e.g., the load balancer). + $directIp = $this->server->get('REMOTE_ADDR'); + + // Perform the same basic validation on the direct IP. + return filter_var($directIp, FILTER_VALIDATE_IP) ? $directIp : null; + } + + /** + * Determine if the current request is expecting a JSON response. + * Primarily checks the 'Accept' header for application/json. + * Also returns true for any AJAX request that doesn't explicitly reject JSON. + */ + public function wants_json(): bool { + // 1. Get the 'Accept' header from the request. + $acceptHeader = $this->headers->get('Accept'); + + // 2. Check if the header explicitly contains 'application/json'. + // The header can be a comma-separated list of types, e.g., "text/html, application/json" + if (false !== strpos($acceptHeader, 'application/json')) { + return true; + } + + // 3. If there's no explicit 'Accept' header, or it's wildcard (*/*), + // we can make an assumption for AJAX requests that they likely want JSON. + // This is a common convention for API calls that don't set a specific Accept header. + if (empty($acceptHeader) || $acceptHeader === '*/*') { + return $this->is_xml_http_request(); + } + + // 4. If none of the above conditions are met, the client wants something else (like HTML). + return false; + } + + public function user_agent(): ?string { + return $this->headers->get('User-Agent'); + } } \ No newline at end of file diff --git a/src/classes/http/response.php b/src/classes/http/response.php index 789742d..0a282de 100644 --- a/src/classes/http/response.php +++ b/src/classes/http/response.php @@ -11,19 +11,13 @@ namespace CodeHydrater\http; class response { - protected string $content; - protected int $status_code; - protected array $headers; + use \CodeHydrater\traits\Macroable; public function __construct( - string $content = '', - int $status_code = 200, - array $headers = [] - ) { - $this->content = $content; - $this->status_code = $status_code; - $this->headers = $headers; - } + protected string $content = '', + protected int $status_code = 200, + protected array $headers = [] + ) { } public function send(): void { http_response_code($this->status_code); diff --git a/src/classes/parameter_bag.php b/src/classes/parameter_bag.php new file mode 100644 index 0000000..bfb8e30 --- /dev/null +++ b/src/classes/parameter_bag.php @@ -0,0 +1,27 @@ + + * @copyright (c) 2025, Robert Strutts + * @license MIT + */ +namespace CodeHydrater; + +/** + * Description of parameter_bag + * Used by: http/request + * @author Robert Strutts + */ +class parameter_bag { + public function __construct(private array $parameters = []) { } + + public function get($key, $default = null) { + return $this->parameters[$key] ?? $default; + } + + public function has($key) { + return array_key_exists($key, $this->parameters); + } +}