Skip to content

Commit 11e9e03

Browse files
"Added Response and Request classes, because apparently I'm a masochist who enjoys writing PHP."
1 parent 429ddf6 commit 11e9e03

File tree

3 files changed

+365
-1
lines changed

3 files changed

+365
-1
lines changed
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<?php
2+
/*
3+
* Spindle CMS
4+
* Copyright (c) 2025. All rights reserved.
5+
*
6+
* This file is part of the Spindle CMS project — a lightweight, modular PHP content framework derived from OpenCart.
7+
*
8+
* @license GNU General Public License v3.0 (GPL-3.0-or-later)
9+
* @link https://github.com/RandomCoderTinker/Spindle
10+
*/
11+
12+
namespace Spindle\system\library\http;
13+
14+
/**
15+
* Secure Request Handler
16+
*/
17+
class Request
18+
{
19+
public array $get = [];
20+
public array $post = [];
21+
public array $cookie = [];
22+
public array $files = [];
23+
public array $server = [];
24+
25+
public function __construct ()
26+
{
27+
$this->get = $this->clean($_GET);
28+
$this->post = $this->clean($_POST);
29+
$this->cookie = $this->clean($_COOKIE);
30+
$this->files = $this->clean($_FILES);
31+
$this->server = $this->clean($_SERVER);
32+
}
33+
34+
// -- Accessors --------------------------------------------------
35+
36+
public function get (string $key, string $type = ''): mixed
37+
{
38+
return $this->filter($this->get[$key] ?? NULL, $type);
39+
}
40+
41+
public function post (string $key, string $type = ''): mixed
42+
{
43+
return $this->filter($this->post[$key] ?? NULL, $type);
44+
}
45+
46+
public function cookie (string $key, string $type = ''): mixed
47+
{
48+
return $this->filter($this->cookie[$key] ?? NULL, $type);
49+
}
50+
51+
public function file (string $key): mixed
52+
{
53+
return $this->files[$key] ?? NULL;
54+
}
55+
56+
public function server (string $key, string $type = ''): mixed
57+
{
58+
return $this->filter($this->server[$key] ?? NULL, $type);
59+
}
60+
61+
// -- Method + JSON Helpers --------------------------------------
62+
63+
public function method (): string
64+
{
65+
return $_SERVER['REQUEST_METHOD'] ?? 'GET';
66+
}
67+
68+
public function isPost (): bool
69+
{
70+
return $this->method() === 'POST';
71+
}
72+
73+
public function isGet (): bool
74+
{
75+
return $this->method() === 'GET';
76+
}
77+
78+
public function json (): array
79+
{
80+
/**
81+
* static $cache explanation:
82+
* This prevents parsing the same input stream (php://input) multiple times.
83+
* It's evaluated only once per request and stored for reuse.
84+
*/
85+
static $cache = NULL;
86+
87+
if ($cache !== NULL) return $cache;
88+
89+
$raw = file_get_contents('php://input');
90+
$decoded = json_decode($raw, TRUE);
91+
92+
$cache = (json_last_error() === JSON_ERROR_NONE && is_array($decoded))
93+
? $this->clean($decoded)
94+
: [];
95+
96+
return $cache;
97+
}
98+
99+
// -- Sanitization & Filtering -----------------------------------
100+
101+
private function filter (mixed $value, string $type): mixed
102+
{
103+
return match ($type) {
104+
'string' => is_string($value) ? trim($value) : '',
105+
'int' => filter_var($value, FILTER_VALIDATE_INT),
106+
'float' => filter_var($value, FILTER_VALIDATE_FLOAT),
107+
'bool' => filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE),
108+
'array' => is_array($value) ? $value : [],
109+
default => $value,
110+
};
111+
}
112+
113+
/**
114+
* Sanitize array keys and optionally trim string values.
115+
*/
116+
private function clean ($data): mixed
117+
{
118+
if (is_array($data)) {
119+
$clean = [];
120+
foreach ($data as $key => $value) {
121+
/**
122+
* Explanation for:
123+
* $cleanKey = is_string($key) ? preg_replace('/[^a-zA-Z0-9_\-]/', '', $key) : $key;
124+
* This ensures no special characters in keys — useful for security.
125+
* Prevents header injection, malformed keys, or exploits like `__proto__`.
126+
*/
127+
$cleanKey = is_string($key) ? preg_replace('/[^a-zA-Z0-9_\-]/', '', $key) : $key;
128+
$clean[$cleanKey] = $this->clean($value);
129+
}
130+
131+
return $clean;
132+
}
133+
134+
return is_string($data) ? trim($data) : $data;
135+
}
136+
137+
// -- Raw Access -------------------------------------------------
138+
139+
public function all (): array
140+
{
141+
return array_merge($this->get, $this->post);
142+
}
143+
144+
public function raw (): string
145+
{
146+
return file_get_contents('php://input');
147+
}
148+
149+
// Optional: expose the full original arrays if needed
150+
public function getData (): array
151+
{
152+
return $this->get;
153+
}
154+
155+
public function postData (): array
156+
{
157+
return $this->post;
158+
}
159+
160+
public function cookieData (): array
161+
{
162+
return $this->cookie;
163+
}
164+
165+
public function fileData (): array
166+
{
167+
return $this->files;
168+
}
169+
170+
public function serverData (): array
171+
{
172+
return $this->server;
173+
}
174+
175+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
/*
3+
* Spindle CMS
4+
* Copyright (c) 2025. All rights reserved.
5+
*
6+
* This file is part of the Spindle CMS project — a lightweight, modular PHP content framework derived from OpenCart.
7+
*
8+
* @license GNU General Public License v3.0 (GPL-3.0-or-later)
9+
* @link https://github.com/RandomCoderTinker/Spindle
10+
*/
11+
12+
namespace Spindle\system\library\http;
13+
14+
/**
15+
* Class Response
16+
*
17+
* Stores the response so the correct headers can go out before the response output is shown.
18+
*/
19+
class Response
20+
{
21+
/**
22+
* @var array<int, string>
23+
*/
24+
private array $headers = [];
25+
/**
26+
* @var int
27+
*/
28+
private int $level = 0;
29+
/**
30+
* @var string
31+
*/
32+
private string $output = '';
33+
34+
/**
35+
* Constructor
36+
*
37+
* @param string $header
38+
*/
39+
public function addHeader (string $header, string $value = NULL, bool $replace = FALSE): void
40+
{
41+
if ($value !== NULL) {
42+
$header = "$header: $value";
43+
}
44+
$this->headers[] = $header;
45+
}
46+
47+
/**
48+
* Get Headers
49+
*
50+
* @return array<int, string>
51+
*/
52+
public function getHeaders (): array
53+
{
54+
return $this->headers;
55+
}
56+
57+
/**
58+
* Redirect
59+
*
60+
* @param string $url
61+
* @param int $status
62+
*
63+
* @return void
64+
*/
65+
public function redirect (string $url, int $status = 302): void
66+
{
67+
header('Location: ' . str_replace(['&amp;', "\n", "\r"], ['&', '', ''], $url), TRUE, $status);
68+
exit();
69+
}
70+
71+
/**
72+
* Set Compression
73+
*
74+
* @param int $level
75+
*
76+
* @return void
77+
*/
78+
public function setCompression (int $level): void
79+
{
80+
$this->level = $level;
81+
}
82+
83+
/**
84+
* Set Output
85+
*
86+
* @param string $output
87+
*
88+
* @return void
89+
*/
90+
public function setOutput (string $output): void
91+
{
92+
$this->output = $output;
93+
}
94+
95+
public function setJsonOutput (array $data): void
96+
{
97+
$json = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
98+
99+
if ($json === FALSE) {
100+
$json = json_encode([
101+
'error' => TRUE,
102+
'reason' => 'Invalid JSON',
103+
'message' => json_last_error_msg(),
104+
]);
105+
}
106+
107+
$this->output = $json;
108+
}
109+
110+
/**
111+
* Get Output
112+
*
113+
* @return string
114+
*/
115+
public function getOutput (): string
116+
{
117+
return $this->output;
118+
}
119+
120+
/**
121+
* Compress
122+
*
123+
* @param string $data
124+
* @param int $level
125+
*
126+
* @return string
127+
*/
128+
private function compress (string $data, int $level = 0): string
129+
{
130+
if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)) {
131+
$encoding = 'gzip';
132+
}
133+
134+
if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'x-gzip') !== FALSE)) {
135+
$encoding = 'x-gzip';
136+
}
137+
138+
if (!isset($encoding) || ($level < -1 || $level > 9)) {
139+
return $data;
140+
}
141+
142+
if (!extension_loaded('zlib') || ini_get('zlib.output_compression')) {
143+
return $data;
144+
}
145+
146+
if (headers_sent()) {
147+
return $data;
148+
}
149+
150+
if (connection_status()) {
151+
return $data;
152+
}
153+
154+
$this->addHeader('Content-Encoding: ' . $encoding);
155+
156+
return gzencode($data, $level);
157+
}
158+
159+
/**
160+
* Output
161+
*
162+
* Displays the set HTML output
163+
*
164+
* @return void
165+
*/
166+
public function output (): void
167+
{
168+
if ($this->output) {
169+
$output = $this->level ? $this->compress($this->output, $this->level) : $this->output;
170+
if (!headers_sent()) {
171+
foreach ($this->headers as $header) {
172+
header($header, TRUE);
173+
}
174+
}
175+
echo $output;
176+
}
177+
}
178+
179+
}

public_html/system/engine/init.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
use Spindle\System\Engine\Autoloader;
1616
use Spindle\System\Engine\Config;
1717
use Spindle\system\library\loggers\Log;
18+
use Spindle\system\library\http\Request;
19+
use Spindle\system\library\http\Response;
1820
use Spindle\System\Library\DB\DatabaseManager;
1921

2022
// Debug/Logging Setup
@@ -64,4 +66,12 @@
6466

6567
// Loader
6668
$loader = new Loader($registry);
67-
$registry->set('load', $loader);
69+
$registry->set('load', $loader);
70+
71+
// Request
72+
$request = new Request();
73+
$registry->set('request', $request);
74+
75+
// Response
76+
$response = new Response();
77+
$registry->set('response', $response);

0 commit comments

Comments
 (0)