Robert 4 weeks ago
commit 951ac7ee20
  1. 1
      .gitignore
  2. 4
      README.md
  3. 6
      docs/notes.txt
  4. 87
      src/Bootstrap.php
  5. 248
      src/Framework/App.php
  6. 240
      src/Framework/CORSHandler.php
  7. 48
      src/Framework/CliDefaults.php
  8. 138
      src/Framework/Common.php
  9. 92
      src/Framework/Configure.php
  10. 28
      src/Framework/Console.php
  11. 48
      src/Framework/Container/AutowireContainer.php
  12. 68
      src/Framework/Container/Container.php
  13. 135
      src/Framework/DI.php
  14. 15
      src/Framework/Enum/ExitOnDump.php
  15. 375
      src/Framework/ErrorHandler.php
  16. 14
      src/Framework/Exception/BadMethodCallException.php
  17. 26
      src/Framework/Http/App/AppHandler.php
  18. 20
      src/Framework/Http/Contract/RouterInterface.php
  19. 8
      src/Framework/Http/Exception/NoRouteProviderFoundException.php
  20. 24
      src/Framework/Http/Exception/RouteNotFoundException.php
  21. 8
      src/Framework/Http/Exception/RouteProviderException.php
  22. 106
      src/Framework/Http/HttpFactory.php
  23. 115
      src/Framework/Http/Kernel.php
  24. 29
      src/Framework/Http/MiddlewareQueueHandler.php
  25. 62
      src/Framework/Http/Request.php
  26. 62
      src/Framework/Http/Response.php
  27. 175
      src/Framework/Http/Routing/Router.php
  28. 83
      src/Framework/Http/Routing/RoutingHandler.php
  29. 53
      src/Framework/Http/Stream.php
  30. 78
      src/Framework/Http/Uri.php
  31. 51
      src/Framework/LoadAll.php
  32. 225
      src/Framework/Logger.php
  33. 53
      src/Framework/Middleware/ErrorMiddleware.php
  34. 39
      src/Framework/Middleware/RequestLoggerMiddleware.php
  35. 26
      src/Framework/ParameterBag.php
  36. 109
      src/Framework/Playground/HttpContainer.php
  37. 79
      src/Framework/Playground/JunkForNow.php
  38. 77
      src/Framework/Playground/RouteServiceProvider.php
  39. 492
      src/Framework/Playground/Router.php
  40. 46
      src/Framework/Playground/ServiceProvider.php
  41. 32
      src/Framework/Playground/pipeOp.php
  42. 36
      src/Framework/Registry.php
  43. 213
      src/Framework/Requires.php
  44. 312
      src/Framework/Security.php
  45. 66
      src/Framework/String/StringFacade.php
  46. 208
      src/Framework/String/StringFns.php
  47. 167
      src/Framework/String/mbStringFns.php
  48. 95
      src/Framework/Trait/Security/CsrfTokenFunctions.php
  49. 161
      src/Framework/Trait/Security/SessionHijackingFunctions.php
  50. 208
      src/Framework/Trait/ServerRequestDelegation.php
  51. 22
      src/LICENSE
  52. 180
      src/Psr4AutoloaderClass.php
  53. 8
      src/composer.json
  54. 288
      src/composer.lock

1
.gitignore vendored

@ -0,0 +1 @@
src/vendor

@ -0,0 +1,4 @@
# IOcornerstone PHP 8.5 Framework
Author Robert Strutts
Copyright (c) 2010-2026 MIT

@ -0,0 +1,6 @@
Element Case Style
Properties camelCase
Methods camelCase
Variables camelCase
Constants UPPER_SNAKE_CASE
Classes PascalCase

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright Copyright (c) 2010-2026, Robert Strutts.
* @license MIT
*/
use IOcornerstone\Psr4AutoloaderClass;
use IOcornerstone\Framework\{
Registry as Reg,
Console,
CliDefaults,
ErrorHandler,
APICacheAge,
Common,
Enum\ExitOnDump as endDump,
DI,
Container\AutowireContainer,
LoadAll,
Http\Kernel,
Http\HttpFactory,
Http\RouteServiceProvider,
Log\Logger
};
define("MEMORY_BASELINE", memory_get_usage());
require_once IO_CORNERSTONE_FRAMEWORK . "Psr4AutoloaderClass.php";
$loader = new Psr4AutoloaderClass();
$loader->register();
$loader->addNamespace("IOcornerstone", IO_CORNERSTONE_FRAMEWORK);
if (defined("IO_CORNERSTONE_PROJECT")) {
$loader->addNamespace("Project", IO_CORNERSTONE_PROJECT);
}
define("PSR", IO_CORNERSTONE_FRAMEWORK . 'vendor' . DIRECTORY_SEPARATOR . 'psr' . DIRECTORY_SEPARATOR);
$loader->addNamespace("Psr\Log", PSR . 'log' . DIRECTORY_SEPARATOR . 'src');
$loader->addNamespace("Psr\Container", PSR . 'container' . DIRECTORY_SEPARATOR . 'src');
$loader->addNamespace("Psr\Http\Message", PSR . 'http-message' . DIRECTORY_SEPARATOR . 'src');
$loader->addNamespace("Psr\Http\Server", [
PSR . 'http-server-middleware' . DIRECTORY_SEPARATOR . 'src',
PSR . 'http-server-handler' . DIRECTORY_SEPARATOR . 'src'
]);
function dd($var = 'nothing', endDump $end = endDump::EXIT_AND_STOP)
{
Common::dump($var, $end);
}
function dump($var = 'nothing', endDump $end = endDump::KEEP_WORKING)
{
Common::dump($var, $end);
}
$debug = true; // false in production
$myErrorHandler = new ErrorHandler($debug);
$myErrorHandler->register();
Reg::set('loader', $loader);
Reg::set('di', new DI()); // Initialize our Dependency Injector
Reg::set('container', new AutowireContainer());
Console::setupConsoleVars(); // Copy CLI Args into $_GET
if (defined("IO_CORNERSTONE_PROJECT")) {
LoadAll::init(IO_CORNERSTONE_PROJECT); // Load Configs and Services
}
function isLive(): bool
{
if (Configure::has('IOcornerstone', 'live')) {
$live = Configure::get('IOcornerstone', 'live');
}
if ($live === null) {
$live = true;
}
return (bool) $live;
}
if (Console::isConsole()) {
CliDefaults::init();
}
$kernel = new Kernel()->run();

@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright Copyright (c) 2022, Robert Strutts.
* @license MIT
*/
namespace IOcornerstone\Framework;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use IOcornerstone\Framework\Http\{
Request,
HttpFactory
};
use IOcornerstone\Framework\{
Requires,
Configure,
Common,
Console,
Security
};
use Exception;
class App
{
private $file;
private $class;
private $method;
private $params;
private $middleWare = [];
private Request $request;
private bool $testing = false;
private string $dirClass = ""; // Testing, CLI, or empty
public function __construct() {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$this->request = $request;
$ret = $this->startUp();
if ($ret === false) {
return $this->local404();
}
return $ret;
}
public function getMiddleware()
{
return $this->middleWare;
}
private function GetWithoutFileExtension(string $route): string
{
$dot_position = strpos($route, '.');
if ($dot_position !== false) {
$offset = strlen($route) - $dot_position;
$ext = Common::getStringRight($route, $offset);
if ($ext === ".php") {
return $this->local404();
}
return substr($route, 0, $dot_position);
}
return $route; // No dot found, return full string
}
private function getFirstChunks(string $input): string
{
$parts = explode('/', $input);
$last = array_pop($parts);
$parts = array(implode('/', $parts), $last);
return $parts[0];
}
private function getLastPart(string $input): string
{
$reversedParts = explode('/', strrev($input), 2);
return strrev($reversedParts[0]);
}
/**
* Do not declare a return type here, as it will Error out!!
*/
private function startUp()
{
$full_route = $this->request->getUri()->getPath();
$noFileExt = $this->GetWithoutFileExtension($full_route);
// Find the Route
$route = $this->getFirstChunks($noFileExt);
// Find the Method
$the_method = $this->getLastPart($noFileExt);
$params = $query = $this->request->getUri()->getQuery();
// Now load Route
$is_from_the_controller = true; // TRUE for from Constructor...
return $this->router($route, $the_method, $params);
}
private function getCtrlDir(): string
{
$ctrl = (Console::isConsole()) ? "cli_" : "";
return ($this->testing) ? "test_" : $ctrl;
}
/**
* Gets route and checks if valid
* @param string $route
* @param string $method
* @param bool $is_controller
* @retval action
*/
public function router(?string $route, ?string $method, $params)
{
$ROOT = IO_CORNERSTONE_PROJECT;
$file = "";
$class = "";
if (empty(trim($route))) {
$uri = '/app/' . Configure::get('IOcornerstone', 'default_project');
} else {
$uri = $route;
}
try {
$filtered_uri = Security::filterUri($uri);
} catch (Exception $ex) {
return false;
}
$safeFolders = Requires::filterDirPath(
$this->getFirstChunks($filtered_uri)
);
if (Requires::isDangerous($safeFolders)) {
return false;
}
$safeFolders = rtrim($safeFolders, '/');
if (empty($ROOT)) {
return false;
}
$safeFile = Requires::filterDirPath(
$this->getLastPart($filtered_uri)
);
if (Requires::isDangerous($safeFile)) {
return false;
}
$this->dirClass = $this->getCtrlDir();
$dir = Requires::saferDirExists($ROOT . "{$this->dirClass}Controllers/" . $safeFolders);
// it failed the _cli ctrl check so go back to default site controller
if ($dir === false) {
$dir = Requires::saferDirExists($ROOT . "Controllers/" . $safeFolders);
$this->dirClass = ""; // Reset to empty to use Normal CTRL
if ($dir === false) {
return false;
}
}
$file = Requires::saferFileExists(basename($safeFile) . 'Controller.php', $dir);
if ($file === false || $file === null) {
return false;
}
$class = Security::filterClass($safeFolders) . "\\" . Security::filterClass($safeFile . "Controller");
if (empty(trim($method))) {
$method = ""; // Clear out null if exists
}
if (substr($method, 0, 2) == '__') {
$method = ""; // Stop any magical methods being called
}
if ($method == "init") {
$method = ""; // Stop init methods from being called
}
return $this->action($file, $class, $method, $params);
}
private function local404(): ResponseInterface
{
return (new HttpFactory())->createResponse(404, [], '404 Page - Not Found');
}
/**
* Do controller action
* @param string $file
* @param string $class
* @param string $method
* @retval type
*/
private function action(string $file, string $class, string $method, $params)
{
$safer_file = Requires::saferFileExists($file);
if (!$safer_file) {
return false;
}
if (empty($class)) {
return false;
}
$use_api = false; // Misc::isApi();
$callClass = "\\Project\\" . $this->dirClass . "Controllers\\" . $class;
$controller = new $callClass($this->request);
// Collect controller-level middleware Directly from the controller file, IE: public static array $middleware = [ \Project\classes\auth_middleware::class ];
$this->middleWare = $controller::$middleware ?? [];
if ($method === "error" && str_contains($class, "app") &&
method_exists($controller, $method) === false
) {
//Broken_error();
return false;
}
if ($use_api) {
if (empty($method)) {
$method = "index";
}
$method .= "_api";
if (method_exists($controller, $method)) {
return $controller->$method($params);
} else {
return false;
}
} else {
if (!empty($method) && method_exists($controller, $method)) {
return $controller->$method($params);
} else {
if (empty($method) && method_exists($controller, 'index')) {
return $controller->index($params);
}
}
}
return false;
}
}
// end of app

@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework;
/**
* How Stable is your API???
* Safe maximum: 7200 (2 hours) for cross-browser compatibility.
*/
enum APICacheAge: int {
case DEVELOPMENT = 300; // 5 minutes
case TESTING = 600; // 10 minutes
case STAGING = 1800; // 30 minutes
case PROD = 7200; // 2 hours (Chrome/Chromium LIMIT!)
case STABLE_PROD = 86400; // 24 hours, Max allowed on Firefox!
case MAX = 31536000; // 1 year (theoretical max), Safari: No limit (but has other caching behaviors)
}
class CORSHandler
{
private $exactOrigins;
private $wildcardPatterns;
private $regexPatterns;
private $allowSubdomains;
private $blocked;
public function __construct(
array $config = [],
private bool $allowCredentials = true,
private APICacheAge $cacheAgeForAPI = APICacheAge::DEVELOPMENT
) {
$this->exactOrigins = $config['exact'] ?? [];
$this->wildcardPatterns = $config['wildcards'] ?? [];
$this->regexPatterns = $config['regex'] ?? [];
$this->allowSubdomains = $config['subdomains'] ?? [];
$this->blocked = $config['blocked'] ?? []; // Add your own blocklist
}
/**
* @param string $extra = ", X-Requested-With, X-API-Key"
*/
public function doPreflight(
string $extra = "",
string $requestMethodDefault = "", // should be left empty; expect for testing purposes!!
string $originDefault = "" // should be left empty; expect for testing purposes!!
) {
$requestMethod = $_SERVER['REQUEST_METHOD'] ?? $requestMethodDefault; // Web or CLI
if ($requestMethod === 'OPTIONS') {
$origin = $_SERVER['HTTP_ORIGIN'] ?? $originDefault;
if ($origin && $this->isOriginAllowed($origin)) {
$this->handlePreflight($extra);
}
exit(0);
}
}
public function handle(
string $extra = "",
?string $requestOrigin = null
): bool {
$origin = $requestOrigin ?? ($_SERVER['HTTP_ORIGIN'] ?? '');
if (!$origin || !$this->isValidOriginFormat($origin)) {
return false;
}
if ($this->isOriginAllowed($origin)) {
$this->setCorsHeaders($origin);
return true;
}
// Log unauthorized attempt (for monitoring)
$this->logUnauthorizedOrigin($origin);
return false;
}
private function isOriginAllowed(string $origin): bool
{
if (empty($origin)) {
return false;
}
// 1. Check exact matches
if (in_array($origin, $this->exactOrigins, true)) {
return true;
}
// 2. Check wildcard patterns (e.g., "*.example.com")
foreach ($this->wildcardPatterns as $pattern) {
if ($this->matchesWildcard($origin, $pattern)) {
return true;
}
}
// 3. Check regex patterns
foreach ($this->regexPatterns as $pattern) {
if (preg_match($pattern, $origin)) {
return true;
}
}
// 4. Check subdomain patterns (e.g., allow all subdomains of example.com)
foreach ($this->allowSubdomains as $domain) {
if ($this->isSubdomainOf($origin, $domain)) {
return true;
}
}
return false;
}
private function matchesWildcard(string $origin, string $pattern): bool
{
// Convert wildcard pattern to regex
$regex = '/^' . str_replace(
['\.', '\*', '\?'],
['\.', '.*', '.'],
preg_quote($pattern, '/')
) . '$/i';
return preg_match($regex, $origin) === 1;
}
private function isSubdomainOf(string $origin, string $domain): bool
{
// Extract domain from origin (remove protocol)
$parsed = parse_url($origin);
if (!isset($parsed['host'])) {
return false;
}
$host = $parsed['host'];
// Check if it's exactly the domain
if ($host === $domain) {
return true;
}
// Check if it's a subdomain of the given domain
return substr($host, -strlen($domain) - 1) === ".{$domain}";
}
private function isValidOriginFormat(string $origin): bool
{
// Basic origin format validation
if (!preg_match('/^https?:\/\/[a-zA-Z0-9\-\.]+(:[0-9]+)?$/', $origin)) {
return false;
}
// Ensure it's not a loopback/localhost from external sources
$parsed = parse_url($origin);
if (!$parsed || !isset($parsed['host'])) {
return false;
}
// Block common attack origins
$host = $parsed['host'];
if (in_array($host, $this->blocked)) {
return false;
}
return true;
}
private function setCorsHeaders($origin): void
{
header("Access-Control-Allow-Origin: {$origin}");
header("Access-Control-Allow-Credentials: {$this->allowCredentials}");
// header("Access-Control-Expose-Headers: X-Custom-Header");
}
private function logUnauthorizedOrigin(string $origin): void
{
// Log to file, monitoring service, or security system
error_log(sprintf(
'Unauthorized CORS request from origin: %s at %s',
$origin,
date('Y-m-d H:i:s')
));
}
private function handlePreflight(?string $extra): void
{
$age = (string) $this->cacheAgeForAPI->value;
// Only set preflight headers if origin is allowed
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization {$extra}");
header("Access-Control-Max-Age: {$age}");
}
}
/*
* Useage:
*
// Example 1: Multiple patterns for SaaS application
$corsConfig = [
'exact' => [
'https://app.company.com',
'https://staging.company.com',
],
'wildcards' => [
'https://*.company.com', // All company subdomains
'https://customer-*.company.com', // Customer-specific subdomains
],
'regex' => [
'/^https:\/\/(dev|staging|test)-\d+\.company\.com$/i', // Dynamic environments
],
'subdomains' => [
'company.com', // Allow all HTTPS subdomains of company.com
]
];
// Example 2: Multi-tenant SaaS with customer domains
$corsConfig = [
'exact' => [
'https://app.saasplatform.com',
'https://admin.saasplatform.com',
],
'regex' => [
'/^https:\/\/[a-z0-9-]+\.customerportal\.com$/i', // Customer portals
'/^https:\/\/(staging|dev)\.customer-[0-9]+\.saasplatform\.com$/i',
]
];
// Example 3: Simple pattern for local development
$corsConfig = [
'exact' => [
'http://localhost:3000',
'http://localhost:5173',
],
'wildcards' => [
'http://localhost:*', // All localhost ports
]
];
// Initialize and handle CORS
$cors = new CORSHandler($corsConfig);
$cors->handle();
*/

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\Registry as Reg;
use IOcornerstone\Framework\Middleware\RequestLoggerMiddleware;
use IOcornerstone\Framework\Middleware\ErrorMiddleware;
use IOcornerstone\Framework\Logger;
use Psr\Log\LoggerInterface;
/**
* This file should not be used under HTTP, but in CLI mode
*/
class CliDefaults
{
public static function init(): void {
/* Logger binding */
Reg::get('container')->set(LoggerInterface::class, function () {
return new Logger(
filename: getenv('LOG_CHANNEL') ?: 'system',
maxCount: (int)(getenv('LOG_MAX_LINES') ?: 1000),
minLevel: getenv('LOG_LEVEL') ?: null
);
});
/* Middleware binding */
Reg::get('container')->set(RequestLoggerMiddleware::class, function ($c) {
return new RequestLoggerMiddleware(
$c->get(LoggerInterface::class)
);
});
/* Error middleware config */
Reg::get('container')->set(ErrorMiddleware::class, fn ($c) =>
new ErrorMiddleware(
$c->get(LoggerInterface::class),
getenv('APP_ENV') !== 'production'
)
);
}
}

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\Enum\ExitOnDump as endDump;
use IOcornerstone\Framework\Console;
use IOcornerstone\Framework\String\StringFacade as F;
final class Common
{
/**
* Clear out from memory given variable by Reference!
* @param type $sensitive_data
*/
public static function wipe(#[\SensitiveParameter] &$sensitive_data): void
{
if (function_exists("sodium_memzero")) {
sodium_memzero($sensitive_data);
}
unset($sensitive_data);
}
public static function get_count($i): int
{
return (is_array($i) || is_object($i)) ? count($i) : 0;
}
public static function stringSubPart(string $string, int $offset = 0, ?int $length = null, $encoding = null) {
if ($length === null) {
return F::substr($string, $offset, strlen($string));
} else {
return F::substr($string, $offset, $length);
}
}
/**
* Will get only left part of string by length.
* @param string $str
* @param int $length
* @retval type string or false
*/
public static function getStringLeft(string $str, int $length): false | string {
return self::stringSubPart($str, 0, $length);
}
/**
* Will get only the right part of string by length.
* @param string $str
* @param int $length
* @retval type string or false
*/
public static function getStringRight(string $str, int $length): false | string {
return self::stringSubPart($str, -$length);
}
/**
* Variable Dump and exit
* Configure of security for show_dumps must be true for debugging.
* @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',
endDump $end = endDump::EXIT_AND_STOP
): void {
if (\IOcornerstone\Framework\Configure::get('security', 'show_dumps') !== true) {
return;
}
$isConsole = Console::isConsole();
if (! $isConsole) {
echo "<details>\r\n<summary>Expand to see Var Dump:</summary>";
echo "<p>";
var_dump($var);
echo "</p>";
echo "</details>";
} else {
var_dump($var);
echo PHP_EOL;
}
if ($var === false) {
echo 'It is FALSE!';
} elseif ($var === true) {
echo 'It is TRUE!';
} elseif (is_resource($var)) {
echo 'VAR IS a RESOURCE';
} elseif (is_array($var) && self::get_count($var) == 0) {
echo 'VAR IS an EMPTY ARRAY!';
} elseif (is_numeric($var)) {
echo 'VAR is a NUMBER = ' . $var;
} elseif (empty($var) && !is_null($var)) {
echo 'VAR IS EMPTY!';
} elseif ($var == 'nothing') {
echo 'MISSING VAR!';
} elseif (is_null($var)) {
echo 'VAR IS NULL!';
} elseif (is_string($var)) {
if (! $isConsole) {
echo 'VAR is a STRING = ' . htmlentities($var);
} else {
echo 'VAR is a STRING = ' . $var;
}
} else {
if (! $isConsole) {
echo "<pre style=\"border: 1px solid green; overflow: auto; margin: 0.5em;\">";
print_r($var);
echo '</pre>';
} else {
print_r($var);
echo PHP_EOL;
}
}
if (! $isConsole) {
echo '<br><br>';
}
if ($end === endDump::EXIT_AND_STOP) {
exit;
}
}
/**
* Please note that not all web servers support: HTTP_X_REQUESTED_WITH
* So, you need to code more checks!
* @retval boolean true if AJAX request by JQuery, etc...
*/
public static function is_ajax(): bool {
$http_x_requested_with = $_SERVER['HTTP_X_REQUESTED_WITH'] ?? "";
return (strtolower($http_x_requested_with) === 'xmlhttprequest');
}
public static function nl2br(string $text): string
{
return strtr($text, array("\r\n" => '<br />', "\r" => '<br />', "\n" => '<br />'));
}
}

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework;
final class Configure {
private static $config = [];
private function __construct(){}
private function __clone(){}
// private function __wakeup(){}
private function __destruct(){}
public static function exists()
{ // an Alias to has
return self::has(func_get_args());
}
public static function has(string $name, $key = false): bool
{
if ($key === false) {
return (isset(self::$config[strtolower($name)])) ? true : false;
}
return (isset(self::$config[strtolower($name)][strtolower($key)])) ? true : false;
}
public static function get(string $name, $key = false)
{
if (isset(self::$config[strtolower($name)])) {
$a = self::$config[strtolower($name)];
if ($key === false) {
return $a;
}
if (isset($a[$key])) {
return $a[$key];
}
}
return null;
}
public static function update()
{ // an Alias to set
return self::set(func_get_args());
}
public static function set(string $name, $value): void
{
self::$config[strtolower($name)] = $value;
}
public static function setKey(string $name, string $key, $value): void
{
self::$config[strtolower($name)][strtolower($key)] = $value;
}
public static function addToKey(string $name, string $key, $value): void
{
self::$config[strtolower($name)][strtolower($key)][] = $value;
}
public static function wipe(string $name, $key = false): void
{
if (! self::exists($name, $key)) {
return;
}
if ($key === false) {
self::wipeData(self::$config[strtolower($name)]);
}
self::wipeData(self::$config[strtolower($name)][strtolower($key)]);
}
public static function loadArray(array $a): void
{
if (isset($a) && is_array($a)) {
foreach ($a as $name => $value) {
self::$config[$name] = $value;
}
}
unset($a);
}
private static function wipeData(&$sensitive_data): void
{
if (function_exists("sodium_memzero")) {
sodium_memzero($sensitive_data);
}
unset($sensitive_data);
}
}

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework;
final class Console {
public static function isConsole(): bool
{
if (PHP_SAPI === 'cli') {
return true;
}
return false;
}
public static function setupConsoleVars(): void
{
if (self::isConsole()) {
$argv = $GLOBALS['argv'];
/*
* Remove ScriptName and Route, by slice 2...
* put results into $_GET global
*/
parse_str(implode('&', array_slice($argv, 2)), $_GET);
}
}
}

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Container;
use ReflectionClass;
final class AutowireContainer extends Container
{
public function get(string $id): mixed
{
if ($this->has($id)) {
return parent::get($id);
}
if (!class_exists($id)) {
throw new \RuntimeException("Class {$id} not found");
}
return $this->autowire($id);
}
private function autowire(string $class): object
{
$ref = new ReflectionClass($class);
$ctor = $ref->getConstructor();
if (!$ctor) {
return new $class();
}
$args = [];
foreach ($ctor->getParameters() as $param) {
$type = $param->getType();
if (!$type || $type->isBuiltin()) {
throw new \RuntimeException(
"Cannot autowire {$class}::\${$param->getName()}"
);
}
$args[] = $this->get($type->getName());
}
return $ref->newInstanceArgs($args);
}
}

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Container;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Container\ContainerExceptionInterface;
use IOcornerstone\Framework\Console;
class Container implements ContainerInterface
{
private array $entries = [];
public function set(string $id, callable $factory): void
{
$this->entries[$id] = $factory;
}
public function get(string $id): mixed
{
if (!$this->has($id)) {
throw new class("No entry found for {$id}")
extends \RuntimeException
implements NotFoundExceptionInterface {};
}
try {
return $this->entries[$id]($this);
} catch (\Throwable $e) {
$reset = (Console::isConsole()) ? "\033[0m" : "";
throw new class(
"Error while resolving {$id}" . $reset . PHP_EOL .
$this->dumpExceptionChain($e),
0,
$e
) extends \RuntimeException
implements ContainerExceptionInterface {};
} finally {
// return "";
}
}
public function has(string $id): bool
{
return isset($this->entries[$id]);
}
public function dumpExceptionChain(\Throwable $e): ?string
{
$level = 0;
$out = null;
while ($e !== null) {
$out .= "Level {$level}: " . get_class($e) . PHP_EOL;
$out .= $e->getMessage() . PHP_EOL;
$out .= $e->getFile() . ', on LINE #' . $e->getLine() . PHP_EOL;
// $out .= $e->getTraceAsString() . PHP_EOL;
$out .= str_repeat('-', 80) . PHP_EOL;
$e = $e->getPrevious();
$level++;
}
return $out;
}
}

@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright Copyright (c) 2010-2026, Robert Strutts.
* @license MIT
*/
namespace IOcornerstone\Framework;
final class DI
{
protected $services = [];
// alias to register
public function set(...$args): void {
$this->register(...$args);
}
// alias to get_service
public function get(...$args) {
return $this->get_service(...$args);
}
public function register(string $serviceName, callable $callable): void
{
$this->services[$serviceName] = $callable;
}
public function has(string $serviceName): bool
{
return (array_key_exists($serviceName, $this->services));
}
public function exists(string $serviceName)
{ // an Alias to has
return $this->has($serviceName);
}
/* Note args may be an object or an array maybe more...!
* This will Call/Execute the service
*/
public function get_service(
string $serviceName,
$args = [],
...$more
) {
if ($this->has($serviceName) ) {
$entry = $this->services[$serviceName];
if (is_callable($entry)) {
return $entry($args, $more);
}
$serviceName = $entry;
}
return $this->resolve(c); // Try to Auto-Wire
}
public function get_auto(string $serviceName)
{
if ($this->has($serviceName) ) {
return $this->services[$serviceName]($this);
}
return $this->resolve($serviceName); // Try to Auto-Wire
}
public function __set(string $serviceName, callable $callable): void
{
$this->register($serviceName, $callable);
}
public function __get(string $serviceName)
{
return $this->get_service($serviceName);
}
public function list_services_as_array(): array
{
return array_keys($this->services);
}
public function list_services_as_string(): string
{
return implode(',', array_keys($this->services));
}
// Reflection API Autowiring FROM-> https://www.youtube.com/watch?v=78Vpg97rQwE&list=PLr3d3QYzkw2xabQRUpcZ_IBk9W50M9pe-&index=72
public function resolve(string $serviceName)
{
try {
$reflection_class = new \ReflectionClass($serviceName);
} catch (\ReflectionException $e) {
// if (! is_live()) {
// var_dump($e->getTrace());
// echo $e->getMessage();
// exit;
// } else {
throw new \Exception("Failed to resolve resource: {$serviceName}!");
// }
}
if (! $reflection_class->isInstantiable()) {
throw new \Exception("The Service class: {$serviceName} is not instantiable.");
}
$constructor = $reflection_class->getConstructor();
if (! $constructor) {
return new $serviceName;
}
$parameters = $constructor->getParameters();
if (! $parameters) {
return new $serviceName;
}
$dependencies = array_map(
function(\ReflectionParameter $param) use ($serviceName) {
$name = $param->getName();
$type = $param->getType();
if (! $type) {
throw new \Exception("Failed to resolve class: {$serviceName} becasue param {$name} is missing a type hint.");
}
if ($type instanceof \ReflectionUnionType) {
throw new \Exception("Failed to resolve class: {$serviceName} because of union type for param {$name}.");
}
if ($type instanceof \ReflectionNamedType && ! $type->isBuiltin()) {
return $this->get_auto($type->getName());
}
throw new \Exception("Failed to resolve class {$serviceName} because of invalid param {$param}.");
}, $parameters
);
return $reflection_class->newInstanceArgs($dependencies);
}
}

@ -0,0 +1,15 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Enum;
enum ExitOnDump: int {
case EXIT_AND_STOP = 0;
case KEEP_WORKING = 1;
}

@ -0,0 +1,375 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\Logger;
use ErrorException;
use Throwable;
final class ErrorHandler
{
private bool $hasError = false;
public function __construct(
private bool $debug = false,
) {
define('WORD_WRAP_CHRS', 80); // Letters before Line Wrap on Errors
}
public function register(): void
{
if ($this->debug) {
error_reporting(E_ALL);
} else {
// ini_set('display_errors', 0);
// ini_set('display_startup_errors', 0);
// // DISABLE XDEBUG DISPLAY FEATURES
// if (extension_loaded('xdebug')) {
// ini_set('xdebug.mode', 'off');
// ini_set('xdebug.force_display_errors', 0);
// ini_set('xdebug.show_error_trace', 0);
// // Disable Xdebug temporarily
// if (function_exists('xdebug_disable')) {
// xdebug_disable();
// }
// }
error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);
}
set_error_handler([$this, 'handleError']);
set_exception_handler([$this, 'handleException']);
register_shutdown_function([$this, 'handleShutdown']);
}
public function unregister(): void
{
restore_error_handler();
restore_exception_handler();
}
/**
* Convert PHP errors into ErrorException
*/
public function handleError(
int $severity,
string $message,
string $file,
int $line
): bool {
$this->hasError = true;
if (!(error_reporting() & $severity)) {
return false;
}
throw new ErrorException($message, 0, $severity, $file, $line);
// Don't execute PHP's internal error handler
return true;
}
/**
* Handle uncaught exceptions
*/
public function handleException($e): bool
{
$this->hasError = true;
if (!$e instanceof Throwable) {
$e = new ErrorException('Unknown error');
}
if ($this->isJsonRequest()) {
if ($this->debug) {
$this->renderJsonDebug($e);
} else {
$this->renderJsonProduction($e);
}
return true;
}
if (Console::isConsole()) {
if ($this->debug) {
$this->renderConsole($e);
} else {
$this->renderProductionConsole();
$this->logException($e);
}
return true;
}
http_response_code(500);
if ($this->debug) {
$this->renderDebug($e);
} else {
$this->renderProduction();
$this->logException($e);
}
// Don't execute PHP's internal error handler
return true;
}
/**
* Handle fatal errors (E_ERROR, etc.)
*/
public function handleShutdown(): void
{
$error = error_get_last();
if ($error !== null && $this->isFatal($error['type'])) {
$exception = new ErrorException(
$error['message'],
0,
$error['type'],
$error['file'],
$error['line']
);
$this->handleException($exception);
}
if ($this->hasError && Console::isConsole()) {
exit(1);
}
}
private function isFatal(int $type): bool
{
return in_array($type, [
E_ERROR,
E_PARSE,
E_CORE_ERROR,
E_COMPILE_ERROR,
], true);
}
private function getErrorType(Throwable $e): string
{
if ($e instanceof ErrorException) {
return match ($e->getSeverity()) {
E_ERROR => 'Fatal Error',
E_WARNING => 'Warning',
E_PARSE => 'Parse Error',
E_NOTICE => 'Notice',
E_CORE_ERROR => 'Core Error',
E_CORE_WARNING => 'Core Warning',
E_COMPILE_ERROR => 'Compile Error',
E_COMPILE_WARNING => 'Compile Warning',
E_USER_ERROR => 'User Error',
E_USER_WARNING => 'User Warning',
E_USER_NOTICE => 'User Notice',
// E_STRICT => 'Strict Standards', // Removed from 8.4+
E_RECOVERABLE_ERROR => 'Recoverable Error',
E_DEPRECATED => 'Deprecated',
E_USER_DEPRECATED => 'User Deprecated',
default => 'Unknown PHP Error',
};
}
return match (true) {
$e instanceof \TypeError => 'Type Error',
$e instanceof \ParseError => 'Parse Error',
$e instanceof \Error => 'Fatal Error',
default => 'Exception',
};
}
private function isJsonRequest(): bool
{
$accept = $_SERVER['HTTP_ACCEPT'] ?? '';
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
return str_contains($accept, 'application/json')
|| str_contains($contentType, 'application/json');
}
private function buildTrace(Throwable $e): array
{
$trace = [];
foreach ($e->getTrace() as $i => $frame) {
$trace[] = [
'index' => $i,
'file' => $frame['file'] ?? null,
'line' => $frame['line'] ?? null,
'class' => $frame['class'] ?? null,
'type' => $frame['type'] ?? null,
'function' => $frame['function'] ?? null,
];
if ($i > 10) {
break;
}
}
return $trace;
}
private function setJsonHeaders(int $status_code = 500): void
{
if (!headers_sent()) {
/*
* Allow JavaScript from anywhere. CORS - Cross Origin Resource Sharing
* @link https://manning-content.s3.amazonaws.com/download/f/54fa960-332e-4a8c-8e7f-1eb213831e5a/CORS_ch01.pdf
*/
http_response_code($status_code);
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");
header('Content-Type: application/json; charset=utf-8');
}
}
private function renderJsonDebug(Throwable $e): void
{
$this->setJsonHeaders();
echo json_encode([
'error' => [
'type' => $this->getErrorType($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $this->buildTrace($e),
]
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
private function renderJsonProduction(): void
{
$this->setJsonHeaders();
echo json_encode([
'error' => [
'message' => 'Internal Server Error'
]
]);
}
private function colorFromType(Throwable $e): string
{
$gotType = $this->getErrorType($e);
if (str_contains($gotType, "Warning")) {
return "warning";
} elseif (str_contains($gotType, "Notice")) {
return "notice";
}
return "error";
}
private function renderConsole(Throwable $e): void
{
$colors = [
'error' => "\033[31m", // red
'warning' => "\033[33m", // yellow
'notice' => "\033[36m", // cyan
'reset' => "\033[0m" // reset
];
$message = $e->getMessage();
$type = $this->colorFromType($e);
$color = $colors[$type] ?? $colors['error'];
$gotType = $this->getErrorType($e);
$out = "Uncaught " . $gotType . PHP_EOL;
$out .= wordwrap($message, WORD_WRAP_CHRS, "\n");
$out .= $colors['reset'];
$out .= PHP_EOL. "In File: " . $e->getFile() . PHP_EOL;
$out .= "On Line # " . $e->getLine() . PHP_EOL;
$out .= $e->getTraceAsString() . PHP_EOL;
echo $color . $out . PHP_EOL;
}
private function formatWebMessage(Throwable $e): string
{
$styles = [
'error' => 'uk-alert-danger',
'warning' => 'uk-alert-warning',
'notice' => 'uk-alert-primary'
];
$type = $this->colorFromType($e);
$style = $styles[$type] ?? $styles['error'];
$content = htmlspecialchars((string) $e);
$message = wordwrap($content, WORD_WRAP_CHRS, "<br>\n");
$assets = "/assets/uikit/css/uikit.gradient.min.css";
if (defined("BaseDir") &&
file_exists(BaseDir. "/public/" . $assets)
) {
$msg = '<link rel="stylesheet" href="' . $assets . '" type="text/css" media="all" />';
$msg .= "<div class=\"uk-alert $style\">";
} else {
$msg = "<div style=\"color: red;padding:10px;border:1px solid #f99;margin:10px;\">";
}
$msg .= $message;
$msg .= "</div>";
return $msg;
}
private function renderDebug(Throwable $e): void
{
echo "<h1>Uncaught " . $this->getErrorType($e) . "</h1>";
echo $this->formatWebMessage($e);
}
private function renderProductionConsole(): void
{
echo "An internal error occurred. Please try again later.";
}
/**
* @todo Make error page red, etc...
*/
private function renderProduction(): void
{
echo "<h1 style=\"color: red;\">An internal error occurred. Please try again later.</h1>";
}
private function setLoggerByLevel(Throwable $e): void
{
$errorMessage = (string) $e;
$logger = new Logger();
if ($logger === false) {
return;
}
if ($e instanceof ErrorException) {
switch($e->getSeverity()) {
case E_ERROR:
case E_CORE_ERROR:
case E_COMPILE_ERROR:
case E_USER_ERROR:
case E_RECOVERABLE_ERROR:
case E_PARSE:
$logger->error($errorMessage);
break;
case E_WARNING:
case E_CORE_WARNING:
case E_COMPILE_WARNING:
case E_USER_WARNING:
$logger->warning($errorMessage);
break;
case E_NOTICE:
case E_USER_NOTICE:
$logger->info($errorMessage);
break;
case E_DEPRECATED:
case E_USER_DEPRECATED:
$logger->debug($errorMessage);
break;
default:
$logger->error($errorMessage);
}
return; // Done
}
// Not ErrorException...
$logger->error($errorMessage);
}
private function logException(Throwable $e): void
{
$this->setLoggerByLevel($e);
}
}

@ -0,0 +1,14 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Exception;
class BadMethodCallException extends \Exception
{
}

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Http\App;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use IOcornerstone\Framework\App;
final class AppHandler implements RequestHandlerInterface
{
public function __construct(private App $app) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->app->handle($request);
}
}

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Http\Contract;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
interface RouterInterface
{
public function get(string $path, mixed $handler, array $middleware = []): void;
public function post(string $path, mixed $handler, array $middleware = []): void;
public function put(string $path, mixed $handler, array $middleware = []): void;
public function patch(string $path, mixed $handler, array $middleware = []): void;
public function delete(string $path, mixed $handler, array $middleware = []): void;
public function options(string $path, mixed $handler, array $middleware = []): void;
public function add(string $method, string $path, callable $handler): void;
public function dispatch(ServerRequestInterface $request): array;
}

@ -0,0 +1,8 @@
<?php
namespace IOcornerstone\Framework\Http\Exception;
class NoRouteProviderFoundException extends \Exception
{
}

@ -0,0 +1,24 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Http\Exception;
/**
* Description of RouteNotFoundException
*
* @author Robert Strutts
*/
class RouteNotFoundException extends \Exception
{
public function __construct($method, $path, $code = 0, ?\Throwable $previous = null)
{
$message = "RouteNotFoundException Method: {$method} Path: {$path}";
parent::__construct($message, $code, $previous);
}
}

@ -0,0 +1,8 @@
<?php
namespace IOcornerstone\Framework\Http\Exception;
class RouteProviderException extends \Exception
{
}

@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Http;
use Psr\Http\Message\{
ServerRequestInterface,
ResponseInterface,
StreamInterface,
};
final class HttpFactory
{
public function createServerRequestFromGlobals(): ServerRequestInterface
{
return new Request(
$_SERVER['REQUEST_METHOD'] ?? 'GET',
Uri::fromString($_SERVER['REQUEST_URI'] ?? '/'),
$this->getAllHeaders(),
Stream::fromString(file_get_contents('php://input')),
$_SERVER,
queryParams: $_GET ?? [],
parsedBody: $_POST ?? null,
cookies: $_COOKIE ?? [],
uploadedFiles: $_FILES ?? [],
attributes: [],
protocol: $this->detectProtocol()
);
}
public function createCliRequest(): ServerRequestInterface
{
global $argv;
$method = $argv[1] ?? 'GET';
$path = $argv[2] ?? $argv[1] ?? '/';
// Allow: php index.php /health
if ($path === $method) {
$method = 'GET';
}
return new Request(
$method,
Uri::fromString($path ?? '/'),
[
'REQUEST_URI' => $path,
'REQUEST_METHOD' => $method,
],
Stream::fromString(file_get_contents('php://input')),
$_SERVER,
queryParams: $_GET ?? [],
parsedBody: $_POST ?? null,
cookies: $_COOKIE ?? [],
uploadedFiles: $_FILES ?? [],
attributes: [],
protocol: $this->detectProtocol()
);
}
public function createResponse(
int $status = 200,
array $headers = [],
string $body = ''
): ResponseInterface {
return new Response(
$status,
$headers,
Stream::fromString($body)
);
}
public function createStream(string $content = ''): StreamInterface
{
return Stream::fromString($content);
}
private function detectProtocol(): string
{
if (!isset($_SERVER['SERVER_PROTOCOL'])) {
return '1.1';
}
return str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']);
}
/**
* Portable replacement for getallheaders()
*/
private function getAllHeaders(): array
{
$headers = [];
foreach ($_SERVER as $key => $value) {
if (str_starts_with($key, 'HTTP_')) {
// Convert HTTP_HEADER_NAME to Header-Name
$name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))));
$headers[$name] = [$value];
} elseif (in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'], true)) {
$name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key))));
$headers[$name] = [$value];
}
}
return $headers;
}
}

@ -0,0 +1,115 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Http;
use IOcornerstone\Framework\{
Registry as Reg,
Console,
App
};
use IOcornerstone\Framework\Http\{
HttpContainer,
Routing\RoutingHandler,
Routing\Router,
App\AppHandler,
Exception\NoRouteProviderFoundException,
Exception\RouteProviderException,
};
use IOcornerstone\Framework\Middleware\{
ErrorMiddleware,
RequestLoggerMiddleware
};
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
class Kernel {
protected ContainerInterface $psrContainer;
protected array $middleware = [];
protected array $serviceProviders = [];
public function __construct() {
$this->psrContainer = Reg::get('container');
}
private function registerRoutes(Router $router): void
{
if (! defined("IO_CORNERSTONE_PROJECT")) {
return; // Running CLI from Framework test script
}
if (! class_exists(\Project\Providers\RouteServiceProvider::class)) {
throw new NoRouteProviderFoundException("Class NOT found: Project\Providers\RouteServiceProvider");
}
try {
$routeProviders = new \Project\Providers\RouteServiceProvider();
} catch (\Throwable $e) {
throw new RouteProviderException("Unable to INIT RouteServiceProvider class \r\n" . $e->getMessage());
}
if (! method_exists($routeProviders, "register")) {
throw new NoRouteProviderFoundException("Method NOT found: Project\Providers\RouteServiceProvider::register");
}
try {
$routeProviders->register($router);
} catch (\Throwable $e) {
throw new RouteProviderException("Unable to call register on Route ServiceProvider \r\n" . $e->getMessage());
}
}
private function buildKernel()
{
$router = new Router();
$this->registerRoutes($router);
$fallback = new AppHandler(
$this->psrContainer->get(App::class)
);
return new MiddlewareQueueHandler(
[
$this->psrContainer->get(ErrorMiddleware::class),
$this->psrContainer->get(RequestLoggerMiddleware::class),
],
new RoutingHandler($router, $this->psrContainer, $fallback)
);
}
private function dispatch($kernel): Response
{
$httpFactory = new HttpFactory();
if (Console::isConsole()) {
$createPsr7Request = $httpFactory->createCliRequest();
} else {
$createPsr7Request = $httpFactory->createServerRequestFromGlobals();
}
return $kernel->handle($createPsr7Request);
}
private function emit(Response $response): void
{
http_response_code($response->getStatusCode());
foreach ($response->getHeaders() as $name => $values) {
if (! is_array($values) && ! is_object($values)) {
continue;
}
foreach ($values as $value) {
header("$name: $value", false);
}
}
echo $response->getBody();
}
public function run(): void {
$kernel = $this->buildKernel();
$response = $this->dispatch($kernel);
$this->emit($response);
}
}
//dd($createPsr7Request->getQueryParams()['name']);
//dd($createPsr7Request->getVar()->get("name"));

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Http;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Server\MiddlewareInterface;
final class MiddlewareQueueHandler implements RequestHandlerInterface
{
public function __construct(
private array $middleware,
private RequestHandlerInterface $handler
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
if (empty($this->middleware)) {
return $this->handler->handle($request);
}
$middleware = array_shift($this->middleware);
return $middleware->process($request, $this);
}
}

@ -0,0 +1,62 @@
<?php
namespace IOcornerstone\Framework\Http;
use Psr\Http\Message\{
ServerRequestInterface,
UriInterface,
StreamInterface,
UploadedFileInterface
};
use IOcornerstone\Framework\Trait\ServerRequestDelegation;
use IOcornerstone\Framework\ParameterBag;
final class Request implements ServerRequestInterface
{
use ServerRequestDelegation;
public function __construct(
private string $method,
private UriInterface $uri,
private array $headers,
private StreamInterface $body,
private array $serverParams,
private array $queryParams = [],
private mixed $parsedBody = null,
private array $cookies = [],
private array $uploadedFiles = [],
private array $attributes = [],
private string $protocol = '1.1'
) {}
/**
* Parameter Bags [has and get - methods]
*/
public function getVar(): ParameterBag
{
return new ParameterBag($this->getQueryParams());
}
public function postVar(): ParameterBag
{
return new ParameterBag((array) $this->getParsedBody());
}
public function cookiesVar(): ParameterBag
{
return new ParameterBag($this->getCookieParams());
}
/* =====================
Framework helpers
===================== */
public function isJson(): bool
{
return str_contains(
$this->getHeaderLine('Content-Type'),
'application/json'
);
}
}

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Http;
use Psr\Http\Message\ResponseInterface;
final class Response implements ResponseInterface
{
public function __construct(
private int $status = 200,
private array $headers = [],
private Stream $body = new Stream(STDIN),
private string $protocol = '1.1'
) {}
public function getStatusCode(): int { return $this->status; }
public function withStatus($code, $reasonPhrase = ''): self {
$clone = clone $this;
$clone->status = $code;
return $clone;
}
public function getBody(): Stream { return $this->body; }
public function withBody(\Psr\Http\Message\StreamInterface $body): self {
$clone = clone $this;
$clone->body = $body;
return $clone;
}
public function getHeaders(): array { return $this->headers; }
public function hasHeader($name): bool { return isset($this->headers[strtolower($name)]); }
public function getHeader($name): array { return $this->headers[strtolower($name)] ?? []; }
public function getHeaderLine($name): string { return implode(',', $this->getHeader($name)); }
public function withHeader($name, $value): self {
$clone = clone $this;
$clone->headers[strtolower($name)] = (array)$value;
return $clone;
}
public function withAddedHeader($name, $value): self {
$clone = clone $this;
$clone->headers[strtolower($name)][] = $value;
return $clone;
}
public function withoutHeader($name): self {
$clone = clone $this;
unset($clone->headers[strtolower($name)]);
return $clone;
}
public function getProtocolVersion(): string { return $this->protocol; }
public function withProtocolVersion($version): self {
$clone = clone $this;
$clone->protocol = $version;
return $clone;
}
public function getReasonPhrase(): string { return ''; }
}

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Http\Routing;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
use IOcornerstone\Framework\Http\{
HttpFactory,
Contract\RouterInterface,
Exception\RouteNotFoundException
};
final class Router implements RouterInterface
{
private array $routes = [];
private array $groupMiddlewareStack = [];
private array $groupStack = [];
private array $typeAliases = [
'int' => '\d+',
'slug' => '[a-z0-9-]+',
'alpha'=> '[a-zA-Z]+',
'alnum'=> '[a-zA-Z0-9]+',
'uuid' => '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}',
];
// $router->addTypeAlias('year', '19\d{2}|20\d{2}');
public function addTypeAlias(string $name, string $regex): void
{
$this->typeAliases[$name] = $regex;
}
public function group(string $prefix, callable $callback, array $middleware = []): void
{
$this->groupStack[] = rtrim($prefix, '/');
$this->groupMiddlewareStack[] = $middleware;
$callback($this);
array_pop($this->groupStack);
array_pop($this->groupMiddlewareStack);
}
public function add(
string $method,
string $path,
mixed $handler,
array $middleware = []
): void
{
$prefix = implode('', $this->groupStack);
$path = $prefix . $path;
[$regex, $params] = $this->compilePath($path);
$this->routes[$method][] = [
'regex' => $regex,
'params' => $params,
'handler' => $handler,
'middleware' => $middleware ?? [],
];
}
public function dispatch(ServerRequestInterface $request): array
{
$method = $request->getMethod();
$path = $request->getUri()->getPath();
foreach ($this->routes[$method] ?? [] as $route) {
if (!preg_match($route['regex'], $path, $matches)) {
continue;
}
$params = [];
foreach ($route['params'] as $name) {
$params[$name] = $matches[$name] ?? null;
}
return [
'handler' => $route['handler'],
'params' => $params,
'middleware' => $this->resolveMiddleware($route),
];
}
throw new RouteNotFoundException($method, $path);
}
public function get(string $path, mixed $handler, array $middleware = []): void
{
$this->add('GET', $path, $handler, $middleware);
}
public function post(string $path, mixed $handler, array $middleware = []): void
{
$this->add('POST', $path, $handler, $middleware);
}
public function put(string $path, mixed $handler, array $middleware = []): void
{
$this->add('PUT', $path, $handler, $middleware);
}
public function patch(string $path, mixed $handler, array $middleware = []): void
{
$this->add('PATCH', $path, $handler, $middleware);
}
public function delete(string $path, mixed $handler, array $middleware = []): void
{
$this->add('DELETE', $path, $handler, $middleware);
}
public function options(string $path, mixed $handler, array $middleware = []): void
{
$this->add('OPTIONS', $path, $handler, $middleware);
}
private function compilePath(string $path): array
{
$params = [];
$regex = preg_replace_callback(
'#\{([a-zA-Z_][a-zA-Z0-9_]*)(?:\:([^}?]+))?(\?)?\}#',
function ($matches) use (&$params) {
$name = $matches[1];
$type = $matches[2] ?? '[^/]+';
$optional = isset($matches[3]);
// Resolve aliases
if (isset($this->typeAliases[$type])) {
$type = $this->typeAliases[$type];
}
$params[] = $name;
if ($optional) {
return '(?:/(?P<' . $name . '>' . $type . '))?';
}
return '(?P<' . $name . '>' . $type . ')';
},
$path
);
return ['#^' . $regex . '/?$#', $params];
}
private function resolveMiddleware(array $route): array
{
$middleware = [];
// Group middleware (stacked)
foreach ($this->groupMiddlewareStack as $group) {
$middleware = array_merge($middleware, $group);
}
// Route-level middleware
return array_merge(
$middleware,
$route['middleware'] ?? []
);
}
}

@ -0,0 +1,83 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Http\Routing;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Container\ContainerInterface;
use IOcornerstone\Framework\Http\{
Contract\RouterInterface,
Exception\RouteNotFoundException
};
final class RoutingHandler implements RequestHandlerInterface
{
public function __construct(
private RouterInterface $router,
private ContainerInterface $container,
private RequestHandlerInterface $fallbackHandler // App adapter
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
try {
$route = $this->router->dispatch($request);
} catch (RouteNotFoundException $e) {
// Delegate to your existing controller logic
return $this->fallbackHandler->handle($request);
}
$request = $this->injectRouteAttributes($request, $route['params']);
return $this->invokeHandler($route['handler'], $request);
}
private function injectRouteAttributes(
ServerRequestInterface $request,
array $params
): ServerRequestInterface {
foreach ($params as $key => $value) {
$request = $request->withAttribute($key, $value);
}
return $request;
}
private function invokeHandler(
mixed $handler,
ServerRequestInterface $request
): ResponseInterface {
// Callable (closure or function)
if (is_callable($handler)) {
return $handler($request);
}
// [Controller::class, 'method']
if (is_array($handler) && count($handler) === 2) {
[$class, $method] = $handler;
$obj = new $class($request);
return $obj->$method();
}
// Invokable controller
if (is_string($handler)) {
$controller = $this->container->get($handler);
if (!method_exists($controller, '__invoke')) {
throw new \RuntimeException("Controller [$handler] is not invokable");
}
return $controller($request);
}
throw new \RuntimeException('Invalid route handler');
}
}

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Http;
use Psr\Http\Message\StreamInterface;
final class Stream implements StreamInterface
{
private $resource;
public function __construct($resource)
{
$this->resource = $resource;
}
public static function fromString(string $content): self
{
$r = fopen('php://temp', 'r+');
fwrite($r, $content);
rewind($r);
return new self($r);
}
public function __toString(): string
{
if (!$this->resource) {
return '';
}
$pos = ftell($this->resource);
rewind($this->resource);
$contents = stream_get_contents($this->resource);
fseek($this->resource, $pos);
return $contents ?: '';
}
public function close(): void { fclose($this->resource); }
public function detach() { $r = $this->resource; $this->resource = null; return $r; }
public function getSize(): ?int { return null; }
public function tell(): int { return ftell($this->resource); }
public function eof(): bool { return feof($this->resource); }
public function isSeekable(): bool { return true; }
public function seek($offset, $whence = SEEK_SET): void { fseek($this->resource, $offset, $whence); }
public function rewind(): void { rewind($this->resource); }
public function isWritable(): bool { return true; }
public function write($string): int { return fwrite($this->resource, $string); }
public function isReadable(): bool { return true; }
public function read($length): string { return fread($this->resource, $length); }
public function getContents(): string { return stream_get_contents($this->resource); }
public function getMetadata($key = null) { return null; }
}

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace IOCornerstone\Framework\Http;
use Psr\Http\Message\UriInterface;
final class Uri implements UriInterface
{
public function __construct(
private string $scheme = '',
private string $host = '',
private ?int $port = null,
private string $path = '/',
private string $query = '',
private string $fragment = ''
) {}
public static function fromString(string $uri): self
{
$parts = parse_url($uri);
return new self(
$parts['scheme'] ?? '',
$parts['host'] ?? '',
$parts['port'] ?? null,
$parts['path'] ?? '/',
$parts['query'] ?? '',
$parts['fragment'] ?? ''
);
}
public function __toString(): string
{
$uri = '';
if ($this->scheme) {
$uri .= $this->scheme . '://';
}
if ($this->host) {
$uri .= $this->host;
}
if ($this->port) {
$uri .= ':' . $this->port;
}
$uri .= $this->path;
if ($this->query) {
$uri .= '?' . $this->query;
}
if ($this->fragment) {
$uri .= '#' . $this->fragment;
}
return $uri;
}
public function getScheme(): string { return $this->scheme; }
public function getAuthority(): string { return $this->host; }
public function getUserInfo(): string { return ''; }
public function getHost(): string { return $this->host; }
public function getPort(): ?int { return $this->port; }
public function getPath(): string { return $this->path; }
public function getQuery(): string { return $this->query; }
public function getFragment(): string { return $this->fragment; }
public function withScheme($scheme): self { $c = clone $this; $c->scheme = $scheme; return $c; }
public function withUserInfo($user, $password = null): self { return $this; }
public function withHost($host): self { $c = clone $this; $c->host = $host; return $c; }
public function withPort($port): self { $c = clone $this; $c->port = $port; return $c; }
public function withPath($path): self { $c = clone $this; $c->path = $path; return $c; }
public function withQuery($query): self { $c = clone $this; $c->query = $query; return $c; }
public function withFragment($fragment): self { $c = clone $this; $c->fragment = $fragment; return $c; }
}

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\Requires;
use IOcornerstone\Framework\UseDir;
final class LoadAll
{
public static function init(string $path): void
{
$config_path = $path . "Configs";
if (is_dir($config_path)) {
$cdir = scandir($config_path);
if ($cdir !== false) {
self::doLoop($cdir, $config_path);
}
}
$service_path = $path . "Services";
if (is_dir($service_path)) {
$sdir = scandir($service_path);
if ($sdir !== false) {
self::doLoop($sdir, $service_path);
}
}
}
private static function doLoop(array $cdir, string $path): void
{
foreach($cdir as $key => $file) {
$file = trim($file);
if ($file === '..' || $file === '.') {
continue;
}
$on = substr($file, 0, 3); // grab first three letters
if ($on !== 'on_') {
continue; // It's not ON, so skip it!
}
$file_with_path = $path ."/" . $file;
Requires::secureInclude($file_with_path, UseDir::FIXED);
}
}
}

@ -0,0 +1,225 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\Requires;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Psr\Log\InvalidArgumentException;
final class Logger implements LoggerInterface
{
private $handle = false;
private string $minLevel;
private const LEVEL_PRIORITY = [
LogLevel::EMERGENCY => 600,
LogLevel::ALERT => 550,
LogLevel::CRITICAL => 500,
LogLevel::ERROR => 400,
LogLevel::WARNING => 300,
LogLevel::NOTICE => 250,
LogLevel::INFO => 200,
LogLevel::DEBUG => 100,
];
private const ENV_LEVEL_MAP = [
'production' => LogLevel::ERROR,
'prod' => LogLevel::ERROR,
'staging' => LogLevel::WARNING,
'testing' => LogLevel::NOTICE,
'development' => LogLevel::DEBUG,
'dev' => LogLevel::DEBUG,
'local' => LogLevel::DEBUG,
];
public function __construct(
string $filename = 'system',
int $maxCount = 1000,
?string $minLevel = null
) {
$env = self::detectEnv();
if ($minLevel === null) {
$minLevel = self::ENV_LEVEL_MAP[$env] ?? LogLevel::ERROR;
}
if (!isset(self::LEVEL_PRIORITY[$minLevel])) {
throw new InvalidArgumentException('Invalid log level: ' . $minLevel);
}
$this->minLevel = $minLevel;
// Stop Up Level attacks
if (str_contains($filename, '..')) {
return false;
}
if (! defined("BaseDir")) {
return false;
}
$logDir = Requires::filterDirPath(BaseDir . '/protected/logs');
if (! Requires::isValidFile($logDir)) {
return false;
}
if (!is_dir($logDir)) {
mkdir($logDir, 0775, true);
}
if (! Requires::isValidFile($filename)) {
return false;
}
$safeFileName = Requires::filterFileName($filename);
if ($safeFileName === false || $safeFileName === "") {
return false;
}
$file = $logDir . '/' . $safeFileName . '.log.txt';
if ($maxCount > 1 && $this->getLines($file) > $maxCount) {
@unlink($file);
}
if (! file_exists($file)) {
if (file_put_contents($file, "\n", FILE_APPEND) === false) {
return false;
}
}
@chmod($file, 0660);
@chgrp($file, 'www-data');
if (!is_writable($file)) {
return false;
}
$this->handle = fopen($file, 'ab');
return true;
}
/* ---------------- PSR-3 Core ---------------- */
public function log($level, $message, array $context = []): void
{
if (!is_string($level) || !isset(self::LEVEL_PRIORITY[$level])) {
throw new InvalidArgumentException('Invalid log level');
}
if (
self::LEVEL_PRIORITY[$level]
< self::LEVEL_PRIORITY[$this->minLevel]
) {
return;
}
if (!$this->handle || !is_resource($this->handle)) {
return;
}
$message = $this->interpolate((string)$message, $context);
$time = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
fwrite(
$this->handle,
sprintf("[%s] %s: %s\n", $time, strtoupper($level), $message)
);
}
/* ---------------- Level Methods ---------------- */
public function emergency($message, array $context = []): void
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
public function alert($message, array $context = []): void
{
$this->log(LogLevel::ALERT, $message, $context);
}
public function critical($message, array $context = []): void
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
public function error($message, array $context = []): void
{
$this->log(LogLevel::ERROR, $message, $context);
}
public function warning($message, array $context = []): void
{
$this->log(LogLevel::WARNING, $message, $context);
}
public function notice($message, array $context = []): void
{
$this->log(LogLevel::NOTICE, $message, $context);
}
public function info($message, array $context = []): void
{
$this->log(LogLevel::INFO, $message, $context);
}
public function debug($message, array $context = []): void
{
$this->log(LogLevel::DEBUG, $message, $context);
}
/* ---------------- Helpers ---------------- */
private static function detectEnv(): string
{
return strtolower(
getenv('APP_ENV')
?: ($_ENV['APP_ENV'] ?? 'production')
);
}
private function interpolate(string $message, array $context): string
{
$replace = [];
foreach ($context as $key => $value) {
if (is_scalar($value) || $value instanceof \Stringable) {
$replace['{' . $key . '}'] = (string)$value;
}
}
return strtr($message, $replace);
}
private function getLines(string $file): int
{
if (!file_exists($file)) {
return 0;
}
$lines = 0;
$fh = fopen($file, 'rb');
if (!$fh) {
return 0;
}
while (!feof($fh)) {
$lines += substr_count(fread($fh, 8192), "\n");
}
fclose($fh);
return $lines;
}
public function __destruct()
{
if ($this->handle && is_resource($this->handle)) {
fclose($this->handle);
}
}
}

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use IOcornerstone\Framework\Http\{
Response,
Stream
};
final class ErrorMiddleware implements MiddlewareInterface
{
public function __construct(
private LoggerInterface $logger,
private bool $displayErrors = false
) {}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
try {
return $handler->handle($request);
} catch (\Throwable $e) {
$this->logger->error(
$e->getMessage(),
['exception' => $e]
);
$bodyString = $this->displayErrors
? $this->formatException($e)
: 'Internal Server Error';
$stream = Stream::fromString($bodyString);
return new Response(500, ['Content-Type' => 'text/plain'], $stream);
}
}
private function formatException(\Throwable $e): string
{
return sprintf(
"%s\n\n%s",
$e->getMessage(),
$e->getTraceAsString()
);
}
}

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Middleware;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
final class RequestLoggerMiddleware implements MiddlewareInterface
{
public function __construct(
private LoggerInterface $logger
) {}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface {
$this->logger->info(
'Incoming request {method} {uri}',
[
'method' => $request->getMethod(),
'uri' => (string) $request->getUri(),
]
);
$response = $handler->handle($request);
$this->logger->info(
'Response {status}',
['status' => $response->getStatusCode()]
);
return $response;
}
}

@ -0,0 +1,26 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
/**
* Description of ParameterBag, easy access to has and get methods
* Used by: Http/Request
*/
class ParameterBag {
public function __construct(readonly 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);
}
}

@ -0,0 +1,109 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Http;
class HttpContainer {
protected array $bindings = [];
protected array $instances = [];
public function bind(string $abstract, $concrete = null, bool $shared = false): void
{
if ($concrete === null) {
$concrete = $abstract;
}
$this->bindings[$abstract] = [
'concrete' => $concrete,
'shared' => $shared
];
}
public function singleton(string $abstract, $concrete = null): void
{
$this->bind($abstract, $concrete, true);
}
public function make(string $abstract, array $parameters = [])
{
// Return if already resolved as singleton
if (isset($this->instances[$abstract])) {
return $this->instances[$abstract];
}
// Get the concrete implementation
$concrete = $this->bindings[$abstract]['concrete'] ?? $abstract;
// If it's a closure, resolve it
if ($concrete instanceof \Closure) {
$object = $concrete($this, $parameters);
} elseif (is_string($concrete)) {
// If it's a class name, instantiate it
$object = $this->build($concrete, $parameters);
} else { // Otherwise use as-is
$object = $concrete;
}
// Store if singleton
if (($this->bindings[$abstract]['shared'] ?? false) === true) {
$this->instances[$abstract] = $object;
}
return $object;
}
protected function build(string $class, array $parameters = [])
{
$reflector = new \ReflectionClass($class);
// Check if class is instantiable
if (!$reflector->isInstantiable()) {
throw new \Exception("Class {$class} is not instantiable");
}
// Get the constructor
$constructor = $reflector->getConstructor();
// If no constructor, instantiate without arguments
if ($constructor === null) {
return new $class();
}
// Get constructor parameters
$dependencies = $constructor->getParameters();
$instances = $this->resolveDependencies($dependencies, $parameters);
return $reflector->newInstanceArgs($instances);
}
protected function resolveDependencies(array $dependencies, array $parameters = [])
{
$results = [];
foreach ($dependencies as $dependency) {
// Check if parameter was provided
if (array_key_exists($dependency->name, $parameters)) {
$results[] = $parameters[$dependency->name];
continue;
}
// Get the type hinted class
$type = $dependency->getType();
if ($type && !$type->isBuiltin()) {
$results[] = $this->make($type->getName());
} elseif ($dependency->isDefaultValueAvailable()) {
$results[] = $dependency->getDefaultValue();
} else {
throw new \Exception("Cannot resolve dependency {$dependency->name}");
}
}
return $results;
}
}

@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework;
// https://www.php-fig.org/psr/psr-12/
final class JunkForNow {
/**
* @url https://php.watch/versions/8.5/filter-validation-throw-exception
*/
public static function main(): void {
// filter_var('foobar', FILTER_VALIDATE_EMAIL, FILTER_THROW_ON_FAILURE);
// throws a Filter\FilterFailedException
$data = [
'myNumber' => '15',
];
$filters = [
'component' => [
'filter' => FILTER_VALIDATE_INT,
'flags' => FILTER_THROW_ON_FAILURE,
'options' => [
'min_range' => 1,
'max_range' => 10,
],
],
];
filter_var_array($data, $filters);
$login = "val1dL0gin";
$filtered_login = filter_var($login, FILTER_CALLBACK, ['options' => 'validate_login']);
var_dump($filtered_login);
}
/**
* @todo Remove from MISC.php
*/
public static function post_var(
string $var,
int $filter = FILTER_UNSAFE_RAW,
array|int $options = FILTER_NULL_ON_FAILURE
): mixed {
return filter_input(INPUT_POST, $var, $filter, $options);
}
public static function get_var(
string $var,
int $filter = FILTER_UNSAFE_RAW,
array|int $options = FILTER_NULL_ON_FAILURE
): mixed {
return filter_input(INPUT_GET, $var, $filter, $options);
}
public static function request_var(
string $var,
int $filter = FILTER_UNSAFE_RAW,
array|int $options = FILTER_NULL_ON_FAILURE
): mixed {
if (filter_has_var(INPUT_POST, $var)) {
return self::post_var($var, $filter, $options);
}
if (filter_has_var(INPUT_GET, $var)) {
return self::get_var($var, $filter, $options);
}
return "";
}
function validate_login(string $value): ?string {
if (strlen($value) >= 5 && ctype_alnum($value)) {
return $value;
}
return null;
}
}

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Http;
use \IOcornerstone\Framework\{
Http\ServiceProvider,
Http\Kernel,
Http\Request,
Http\Response,
Router,
App,
};
/**
* Description of RouteServiceProvider
* Setup Router and Controllers
*
*/
class RouteServiceProvider extends ServiceProvider
{
public function __construct(protected Kernel $kernel)
{}
private function build($handler, $response, $request, $next, $controllerMiddleware)
{
// Build middleware stack
$middleware_stack = array_reduce(
array_reverse($controllerMiddleware),
function ($next, $middleware) {
return function ($request, $response) use ($next, $middleware) {
if ($middleware !== null ) {
$instance = new $middleware();
return $instance($request, $response, $next);
}
};
},
function ($request, $response) use ($handler) {
try {
$data = $handler->getContent();
} catch (\Throwable $e) {
throw new \Exception("No Response from [Controller]?");
}
if (! empty($data)) {
$response->setContent($data);
}
return $response;
}
);
return $middleware_stack($request, $response);
}
public function register(): void
{
// Add router middleware
$this->kernel->addMiddleware(function (Request $request, Response $response, $next) {
$returned_route = Router::execute($request, $response);
if ($returned_route["found"] === false) {
$app = new App($request, $response);
$returned = $app->loadController();
$a_middleware = $returned['middleware'] ?? [];
$data = $returned['data'] ?? "";
return $this->build($data, $response, $request, $next, $a_middleware);
} else {
return $this->build($returned_route['returned'], $response, $request, $next, $returned_route['middleware']);
}
});
}
}

@ -0,0 +1,492 @@
<?php
declare(strict_types=1);
/**
* @license MIT
* @copyright (c) 2018, Patrik Mokrý
* @author Patrik Mokrý
* @link https://github.com/MokryPatrik/PHP-Router
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\Http\Request;
use IOcornerstone\Framework\Http\Response;
class router
{
private static $instance = null;
public static $URL = null;
public static $REQUEST = null;
public static $params = [];
private static $queryParams = [];
private static $routes = []; // All routes
public static $route = ""; // Return current route
private static $last = null; // Last route added
private static $prefix = null;
private static $name = null;
private static $doNotIncludeInParams = [];
private static $shortcuts = [
'i' => '(\d+)', // Any Number
's' => '(\w+)', // Any Word
'locale' => '(sk|en)->en'
];
/**
* Init new self instance
*/
public static function init() {
self::$instance = new self();
}
/**
* Assign name to route
*
* @param $name
*/
public static function name($name)
{
$route = self::$routes[self::$last];
unset(self::$routes[self::$last]);
self::$routes[self::$name . $name] = $route;
}
/**
* Add custom shortcut
* shortcut with default value -> $regex = (val1|val2)->val1;
*
* @param $shortcut
* @param $regex
*/
public static function addShortcut($shortcut, $regex)
{
self::$shortcuts[$shortcut] = $regex;
}
/**
* Get link from route name and params
*
* @param $route
* @param array $params
* @param $absolute
* @return null
*/
public static function link($route, $params = [], $absolute = true)
{
if (!isset(self::$routes[$route])) return null;
$route = self::$routes[$route];
$link = "";
foreach ($route['params'] as $key => $param) {
if (isset($param['real'])) {
$link .= $param['pattern'] . '/';
} else if (isset($params[$param['name']])) {
// Chcek if param has default
if (isset($param['default']) && $param['default'] !== $params[$param['name']]) {
$link .= $params[$param['name']] . '/';
}
}
}
// Add absolute path
if ($absolute) {
$link = self::$URL . $link;
}
// Cut slash at the end
return rtrim($link, '/');
}
/**
* Redirect to specific route or defined location
*
* @param $route
* @param array $params
*/
public static function redirect($route, $params = [])
{
if (isset(self::$routes[$route])) {
$route = self::link($route, $params);
}
header('Location: ' . $route, true);
die();
}
/**
* Create prefixed routes
*
* @param $prefix
* @param $callback
* @param string $name
* @param array $doNotIncludeInParams
*/
public static function prefix($prefix, $callBack, $name = '', $doNotIncludeInParams = [])
{
self::$prefix = $prefix;
self::$name = $name;
self::$doNotIncludeInParams = $doNotIncludeInParams;
call_user_func($callBack);
self::$prefix = null;
self::$name = null;
self::$doNotIncludeInParams = [];
}
/**
* Create route
*
* @param $route
* @param $action
* @param array $method
* @return Router
*/
public static function route($route, $action, $method = ["POST", "GET"])
{
$prefix = self::$prefix;
if (empty($route) && !empty(self::$prefix)) {
$prefix = rtrim(self::$prefix, '/');
}
$explodedRoute = explode("/", $prefix . $route);
$params = [];
$pattern = "";
// create route
foreach ($explodedRoute as $key => $r) {
if (strpos($r, '}?') !== false) {
$r = self::dynamic($r, $params, true);
$pattern = substr($pattern, 0, -1);
$dyn = true;
} else if (strpos($r, '}') !== false) {
$r = self::dynamic($r, $params, false);
$pattern = substr($pattern, 0, -1);
$dyn = true;
} else {
$params[] = [
'pattern' => $r,
'real' => false
];
}
$pattern .= ($key == 0 && !isset($dyn) ? '/' : '') . $r . '/';
}
// Create pattern
$pattern = substr($pattern, 0, -1) . '/?';
$pattern = '~^' . str_replace('/', '\/', $pattern) . '$~';
// Save data to static property
$name = uniqid();
self::$routes[$name] = [
'route' => $route,
'pattern' => $pattern,
'params' => $params,
'action' => $action,
'method' => $method,
'doNotIncludeInParams' => self::$doNotIncludeInParams
];
// Set last added
self::$last = $name;
// return self instance
if (self::$instance == null) {
self::init();
}
return self::$instance;
}
public static function get($route, $action) {
return self::route($route, $action, ['GET']);
}
public static function post($route, $action) {
return self::route($route, $action, ['POST']);
}
public static function put($route, $action) {
return self::route($route, $action, ['PUT']);
}
public static function delete($route, $action) {
return self::route($route, $action, ['DELETE']);
}
public static function any($route, $action) {
return self::route($route, $action, ['GET', 'POST', 'PUT', 'DELETE']);
}
/**
* Create all routes for resource (CRUD)
*
* @param $route
* @param $controller
* @param string $name
* @param string $shortcut
*/
public static function resource($route, $controller, $name = null, $shortcut = 's')
{
self::$name = self::$name . $name;
self::get($route . '/{page::paginator}?', $controller . "@index")->name('index');
self::get($route . "/create", $controller . "@create")->name('create');
self::post($route, $controller . "@store")->name('store');
self::get($route . "/{id::" . $shortcut . "}", $controller . "@show")->name('show');
self::get($route . "/{id::" . $shortcut . "}/edit", $controller . "@edit")->name('edit');
self::put($route . "/{id::" . $shortcut . "}", $controller . "@update")->name('update');
self::delete($route . "/{id::" . $shortcut . "}", $controller . "@destroy")->name('destroy');
self::get($route . "/{id::" . $shortcut . "}/delete", $controller . "@destroy")->name('destroy_');
self::$name = str_replace($name, '', self::$name);
}
/**
* Handle dynamic parameter in route
*
* @param string $route
* @param array $params
* @param boolean $optional
* @return mixed
*/
private static function dynamic($route, &$params, $optional = false)
{
$shortcut = self::rules($route);
$name = str_replace('{', '', $route);
if (!$optional) {
$name = str_replace('}', '', $name);
} else {
$name = str_replace('}?', '', $name);
}
if (strpos($name, '::')) {
$name = substr($name, 0, strpos($name, "::"));
}
if (array_search($name, array_column($params, 'name')) === false) {
$pattern = str_replace('(', '(/', $shortcut['shortcut']);
$pattern = str_replace('|', '|/', $pattern);
// If is optional add ? at the end of pattern
if ($optional) {
$pattern .= '?';
}
$params[] = [
'name' => $name,
'pattern' => $pattern,
'default' => $shortcut['default']
];
} else {
throw new \Exception('Parameter with name ' . $name . ' has been already defined');
}
return $pattern;
}
/**
* Dynamic route rules
*
* @param $route
* @return mixed
*/
private static function rules($route)
{
if (preg_match('~::(.*?)}~', $route, $match)) {
list(, $shortcut) = $match;
if (isset(self::$shortcuts[$shortcut])) {
// Try to get default value from shortcut
$default = false;
$shortcut = self::$shortcuts[$shortcut];
if (preg_match('~->(.*?)$~', $shortcut, $match)) {
$default = $match[1];
$shortcut = str_replace($match[0], '', $shortcut);
}
// Return shortcut and default value
return [
'shortcut' => $shortcut,
'default' => $default
];
}
}
return [
'shortcut' => self::$shortcuts['s'],
'default' => false
];
}
/**
* get_all_routes -> Added by Robert Strutts to auto load routes
* Namespace must start with Project
* Needs a routes folder, then
* either an routes.php or test_routes.php files must exists
* with a method called get!
*/
private static function get_all_routes(bool $testing = false) {
$route_name = (console_app::is_cli()) ? "cli_routes" : "routes";
$routes = ($testing) ? "test_routes" : $route_name;
$routes_class = "\\Project\\routes\\{$routes}";
if (class_exists($routes_class)) {
if (method_exists($routes_class, "get")) {
$callback = "{$routes_class}::get";
call_user_func($callback);
}
}
}
/**
* Execute router
*/
public static function execute(Request $my_request, Response $my_response) {
$ROOT = bootstrap\site_helper::get_root();
$request_uri = bootstrap\site_helper::get_uri();
$request_method = bootstrap\site_helper::get_method();
$testing = bootstrap\site_helper::get_testing();
// Generate request and absolute path
self::generateURL($ROOT, $request_uri);
// Fecth all Routes from the Project
self::get_all_routes($testing);
// Get query string
$request = explode('?', $request_uri);
$queryParams = [];
if (isset($request[1])) {
$queryParams = $request[1];
parse_str($queryParams, $queryParams);
self::$queryParams = $queryParams;
}
// Modify request
$request = '/' . trim(self::$REQUEST, '/');
// Find route
foreach (self::$routes as $routeKey => $route) {
$post_method = misc::post_var("_method");
$matchMethod = in_array($request_method, $route['method']) || ($post_method !== null
&& in_array($post_method, $route['method']));
if (preg_match($route['pattern'], $request, $match) && $matchMethod) {
// Default variables
$explodedRequest = explode('/', ltrim($request, '/'));
$routeParams = $route['params'];
// Match request params with params in array - static params
foreach ($explodedRequest as $key => $value) {
foreach ($routeParams as $k => $routeParam) {
// Go to the next request part
if (isset($routeParam['real']) && $routeParam['pattern'] == $value) {
unset($routeParams[$k]);
unset($explodedRequest[$key]);
break;
}
}
}
$number = "(/\d+)";
$params = [];
// Match request params with params in array - dynamic params
foreach ($routeParams as $k => $routeParam) {
$value = $explodedRequest[$k] ?? false;
$default = $routeParam['default'] ?? true;
$pattern = $routeParam['pattern'] ?? "";
$wild = str_contains($pattern, "?");
if (! $wild && ($value === false && $default)) {
$params[$routeParam['name']] = null;
} else if ($wild && ($value === false && ! $default)) {
// Let the default function params be used by not setting anything here!
} else if (preg_match('~' . $pattern . '~', '/' . $value, $match)) {
if (str_contains($pattern, $number)) {
$params[$routeParam['name']] = (int) $value;
} else {
$params[$routeParam['name']] = $value;
}
unset($routeParams[$k]);
}
}
// Merge with query params
self::$params = array_merge($params, $queryParams);
// Setup default route and url
self::$route = $route;
foreach ($params as $key => $value) {
if (in_array($key, $route['doNotIncludeInParams']) && !is_numeric($key)) {
unset($params[$key]);
}
}
// Call action
if (is_callable($route['action'])) {
$returned = call_user_func_array($route['action'], $params);
return ["found"=> true, "returned"=> $returned];
} else if (strpos($route['action'], '@') !== false) {
// call controller
list($controller, $method) = explode('@', $route['action']);
// init new controller
$controller = new $controller($my_request, $my_response);
// Collect controller-level middleware
$controller_middleware = $controller::$middleware ?? [];
// Check if class has parent
$parentControllers = class_parents($controller);
if (!empty($parentControllers)) {
end($parentControllers);
$parentController = $parentControllers[key($parentControllers)];
$parentController = new $parentController($my_request, $my_response);
// Add properties to parent class
foreach ($params as $key => $value) {
$parentController::$params[$key] = $value;
}
}
//Call method
if (method_exists($controller, $method)) {
$returned = call_user_func_array([$controller, $method], $params);
return ["found"=> true, "returned"=> $returned, "middleware"=>$controller_middleware];
}
}
}
}
return ["found"=>false];
}
/**
* Generate URL
*
* @param $ROOT
*/
private static function generateURL(string $ROOT, string $request_uri)
{
$https = bootstrap\safer_io::get_clean_server_var('HTTPS');
$baseLink = ($https === 'on') ? "https" : "http";
$server_name = bootstrap\safer_io::get_clean_server_var('SERVER_NAME');
$baseLink .= "://" . $server_name;
$port = bootstrap\safer_io::get_clean_server_var('SERVER_PORT');
$baseLink .= ($port !== '80') ? ':' . $port : ':';
$baseRequest = '';
$request = $request_uri;
foreach (explode('/', $ROOT) as $key => $value) {
if ($value == '') continue;
if (preg_match('~/' . $value . '~', $request)) {
$baseRequest .= $value . '/';
}
$request = preg_replace('~/' . $value . '~', '', $request, 1);
}
self::$URL = $baseLink . '/' . $baseRequest;
self::$REQUEST = explode('?', $request)[0];
}
}

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2025, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Http;
abstract class ServiceProvider {
protected kernel $kernel;
public function __construct(kernel $kernel)
{
$this->kernel = $kernel;
}
abstract public function register(): void;
}
/**
* Example Useage:
namespace App\Providers;
use IOcornerstone\Framework\Http\kernel;
use IOcornerstone\Framework\Http\service_provider;
use App\Services\Database;
class app_service_provider extends service_provider
{
public function register(): void
{
$this->kernel->getContainer()->singleton(Database::class, function() {
return new Database(
getenv('DB_HOST'),
getenv('DB_USER'),
getenv('DB_PASS'),
getenv('DB_NAME')
);
});
}
}
*/

@ -0,0 +1,32 @@
<?php
//echo trim(strtoupper(str_rot13(str_shuffle("Hello, World!"))));
/**
* yonks: noun, A long time (especially a longer time than expected); ages.
* Here its a lot of useless data passed
*/
function yonks(string $x): string
{
return hash('sha256', $x);
}
/**
* PHP 8.5 adds a new operator, the pipe operator (|>)
* to chain multiple callables from left to right,
* taking the return value of the left callable and
* passing it to the right.
*
* They are suitable when all of the functions in the
* chain require only one parameter, have return values,
* and do not accept by-reference parameters.
echo trim("Hello, World!")
|> strtoupper(...)
|> str_shuffle(...)
|> str_rot13(...)
|> yonks(...);
*
*/

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework;
final class Registry {
private static $registry = [];
protected function __construct() {
}
public static function get(string $name, $key = false) {
if (isset(self::$registry[strtolower($name)])) {
$a = self::$registry[strtolower($name)];
if ($key === false) {
return $a;
}
if (isset($a[$key])) {
return $a[$key];
}
}
return null;
}
public static function set(string $name, $value): bool {
if (array_key_exists(strtolower($name), self::$registry)) {
return false;
}
self::$registry[strtolower($name)] = $value;
return true;
}
}

@ -0,0 +1,213 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright Copyright (c) 2022, Robert Strutts.
* @license MIT
*/
namespace IOcornerstone\Framework;
enum UseDir: string {
case FIXED = "Fixed";
case FRAMEWORK = "Framework";
case PROJECT = "Project";
case ONERROR = "OnError";
}
final class Requires {
private static $loadedFiles = [];
public static function getLoadedFiles(): array {
return self::$loadedFiles;
}
public static function isValidFile(string $fileName): bool {
if (is_string($fileName) && strlen($fileName) < 64) {
return (self::isDangerous($fileName) === false) ? true : false;
}
return false;
}
public static function isDangerous(string $file): bool {
// Make sure the file does not contain null bytes to avoid PHAR file attacks
if (strpos($file, "\x00") !== false) {
return true;
}
// Remove non-visible characters
$file = preg_replace('/[\x00-\x1F\x7F]/u', '', $file);
if (strpos($file, "..") !== false || strpos($file, "./") !== false) {
return true; // .. Too dangerious, up path attack
}
/*
* :// Too dangerious, PHAR file execution of serialized code injection, etc...
* Also, prevent remote code execution from http://, ftp://
*/
if (strpos($file, "://") !== false) {
return true;
}
return false;
}
public static function filterFileName(string $file): string {
return preg_replace('/[^-a-z0-9._\\/\s]+/i', "", $file);
}
/**
* Do not allow periods in folder names!
* @param string $dir
* @return string
*/
public static function filterDirPath(string $dir): string {
return preg_replace('/[^-a-z0-9_\\/]+/i', "", $dir);
}
private static function getPHPVersionForFile(string $file): string|bool {
if (file_exists($file . PHP_MAJOR_VERSION . PHP_MINOR_VERSION) && is_readable($file . PHP_MAJOR_VERSION . PHP_MINOR_VERSION)) {
return $file . PHP_MAJOR_VERSION . PHP_MINOR_VERSION;
} elseif (file_exists($file . PHP_MAJOR_VERSION) && is_readable($file . PHP_MAJOR_VERSION)) {
return $file . PHP_MAJOR_VERSION;
} elseif (file_exists($file) && is_readable($file)) {
return $file;
} else {
return false;
}
}
/**
* @return Cleaned DIR or False
*/
public static function saferDirExists(string $dir): string|bool {
if (self::isDangerous($dir) || empty($dir)) {
return false;
}
$dir = str_replace('_DS_', '/', $dir);
$dir = self::filterDirPath($dir);
$realpath = realpath($dir);
if ($realpath === false) {
return false;
}
$dir = escapeshellcmd($realpath);
return (file_exists($dir)) ? $dir : false;
}
private static function stringLastPosition(string $string, string $needle, int $offset = 0, $encoding = null) {
return (extension_loaded('mbstring')) ? mb_strrpos($string, $needle, $offset, $encoding) : strrpos($string, $needle, $offset);
}
private static function stringSubPart(string $string, int $offset = 0, int $length = null, $encoding = null) {
if ($length === null) {
return (extension_loaded('mbstring')) ? mb_substr($string, $offset,mb_strlen($string), $encoding) : substr($string, $offset, strlen($string));
} else {
return (extension_loaded('mbstring')) ? mb_substr($string, $offset, $length, $encoding) : substr($string, $offset, $length);
}
}
/**
* @return Cleaned file name or false
*/
public static function saferFileExists(string $file, string $dir=""): string|bool {
if (self::isDangerous($file)) {
return false;
}
if (self::isDangerous($dir)) {
return false;
}
if (empty($file)) {
return false;
}
$posLastOccurrence = self::stringLastPosition($file, ".");
if ($posLastOccurrence === false) {
$fileType = ".php";
$fileName = $file;
} else {
$fileName = self::stringSubPart($file, 0, $posLastOccurrence);
// Keep offset negitive, to get file kind...
$fileKind = self::stringSubPart($file, -(strlen($file) - $posLastOccurrence));
$fileType = match ($fileKind) {
".twig" => ".twig",
".tpl" => ".tpl",
default => ".php",
};
}
$filteredFile = self::filterFileName($fileName);
if (empty($dir)) {
$filePlusDir = $filteredFile;
} else {
$filteredDir = rtrim(self::filterDirPath($dir), '/') . '/';
$filePlusDir = $filteredDir . $filteredFile;
}
$escapedFile = escapeshellcmd($filePlusDir . $fileType);
return self::getPHPVersionForFile($escapedFile);
}
private static function obStarter() {
if (extension_loaded('mbstring')) {
ob_start('mb_output_handler');
} else {
ob_start();
}
}
private static function secureFile(bool $returnContents, string $file, UseDir $path, $local = null, array $args = array(), bool $loadOnce = true) {
$dir = match ($path) {
UseDir::FIXED => "",
UseDir::FRAMEWORK => IO_CONERSTONE_FRAMEWORK,
UseDir::ONERROR => IO_CONERSTONE_PROJECT . "views/on_error/",
default => IO_CONERSTONE_PROJECT,
};
$versionedFile = self::saferFileExists($file, $dir);
if ($versionedFile === false) {
return false;
}
if (is_array($args)) {
if (isset($args["return_contents"])
|| isset($args["script_output"])
|| isset($args["versioned_file"])
) {
return false; // Args are Dangerious!! Abort
}
extract($args, EXTR_PREFIX_SAME, "dup");
}
if (defined('CountFiles') && CountFiles) {
self::$loadedFiles[] = $versionedFile;
}
if ($returnContents) {
$scriptOutput = (string) ob_get_clean();
self::obStarter();
include $versionedFile;
$scriptOutput .= (string) ob_get_clean();
return $scriptOutput;
} else {
return ($loadOnce) ? include_once($versionedFile) : include($versionedFile);
}
}
/**
* Use secure_include instead of include.
* @param string $file script
* @param UseDir $path (framework or project)
* @param type $local ($this)
* @param array $args ($var to pass along)
* @param bool $load_once (default true)
* @return results of include or false if failed
*/
public static function secureInclude(string $file, UseDir $path = UseDir::PROJECT, $local = null, array $args = array(), bool $loadOnce = true) {
return self::secureFile(false, $file, $path, $local, $args, $loadOnce);
}
public static function secureGetContent(string $file, UseDir $path = UseDir::PROJECT, $local = null, array $args = array(), bool $loadOnce = true) {
return self::secureFile(true, $file, $path, $local, $args, $loadOnce);
}
}

@ -0,0 +1,312 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\{
Configure,
Requires
};
/**
* Description of Security
*
* @author Robert Strutts
*/
class Security
{
use \IOcornerstone\Framework\Trait\Security\CsrfTokenFunctions;
use \IOcornerstone\Framework\Trait\Security\SessionHijackingFunctions;
/**
* Get unique IDs for database
* @return int
*/
public static function getUniqueNumber(): int {
return abs(crc32(microtime()));
}
/**
* Get token
* @return string
*/
public static function getUniqueId(): string {
$moreEntropy = true;
$prefix = ""; // Blank is a rand string
return md5(uniqid($prefix, $moreEntropy));
}
public static function useHmac(string $algo, string $pepper) {
if (!function_exists("hash_hmac_algos")) {
throw new \Exception("hash_hmac not installed!");
}
if (strpos($algo, "md") !== false) {
throw new \Exception("MD is too weak!");
}
if (strpos($algo, "sha1") !== false) {
throw new \Exception("sha1 is too weak!");
}
$allowed = hash_hmac_algos();
if (in_array(strtolower($algo), $allowed)) {
return hash_hmac($algo, $pepper);
}
throw new \Exception("hmac algo not found!");
}
/*
* Consider MD5 and SHA1 which are fast and efficient,
* making them ideal for check summing and file verification.
* However, their speed makes them unsuitable for hashing a
* user’s password. With today’s computational power of
* modern CPUs/GPUs and cloud computing, a hashed password
* can be cracked by brute force in a matter of minutes.
* It’s fairly trivial to quickly generate billions of MD5
* hashes from random words until a match is found, thereby
* revealing the original plain-text password. Instead,
* intentionally slower hashing algorithms such as bcrypt
* or Argon2 should be used.
*/
public static function findDefaultHashAlgo() {
if (defined("PASSWORD_ARGON2ID"))
return PASSWORD_ARGON2ID;
if (defined("PASSWORD_ARGON2"))
return PASSWORD_ARGON2;
if (defined("PASSWORD_DEFAULT"))
return PASSWORD_DEFAULT;
if (defined("PASSWORD_BCRYPT"))
return PASSWORD_BCRYPT;
return false;
}
private static function isValidHashAlgo($algo): bool {
return (in_array($algo, password_algos()));
}
/*
* The password_hash() function not only uses a secure
* one-way hashing algorithm, but it automatically handles
* salt and prevents time based side-channel attacks.
*/
public static function do_password_hash(#[\SensitiveParameter] string $password): bool | string {
$pwdPeppered = self::makeHash($password);
$hashAlgo = Configure::get(
"security",
"hash_algo"
) ?? false;
if ($hashAlgo === false) {
throw new \Exception("Security Hash Algo not set!");
}
if (! self::isValidHashAlgo($hashAlgo)) {
throw new \Exception("Invalid Security Hash Alogo set");
}
return password_hash($pwdPeppered, $hashAlgo);
}
public static function doPasswordVerify(
#[\SensitiveParameter] string $inputPwd, #[\SensitiveParameter] $dbPassword
): bool {
$pwdPeppered = self::makeHash($inputPwd);
return password_verify($pwdPeppered, $dbPassword);
}
/**
* Make a secure Hash
* @param string $text to encode
* @param string $level (weak, low, high, max)
* @return string new Hashed
*/
public static function makeHash(#[\SensitiveParameter] string $text): string {
$level = Configure::get('security', 'hash_level');
if (empty($level)) {
$level = "normal";
}
$pepper = Configure::get('security', 'pepper_pwd');
if (strlen($pepper) < 12) {
throw new \Exception("Pepper Password, too short!");
}
$salt = Configure::get('security', 'salt_pwd');
if (strlen($salt) < 5) {
throw new \Exception("Salt Password, too short!");
}
switch (strtolower($level)) {
case 'max':
// Prefer computing using HMAC
if (function_exists("hash_hmac")) {
return hash_hmac("sha512", $text, $pepper);
}
// Sha512 hash is the next best thing
if (function_exists("hash")) {
return hash("sha512", $salt . $text . $pepper);
}
case 'normal':
// Prefer computing using HMAC
if (function_exists("hash_hmac")) {
return hash_hmac("sha256", $text, $pepper);
}
// Sha256 hash is the next best thing
if (function_exists("hash")) {
return hash("sha256", $salt . $text . $pepper);
}
case 'weak':
throw \Exception("Too weak of a Hash FN");
// return sha1($salt . $text . $pepper);
case 'low':
throw \Exception("Too weak of a Hash FN");
// return md5($salt . md5($text . $pepper));
default:
break;
}
return self::useHmac($level, $pepper);
}
/**
* @method filter_class
* @param type $class
* Please NEVER add a period or SLASH as it will allow BAD things!
* IT should be a-zA-Z0-9_ and that's it.
* @retval string of safe class name
*/
public static function filterClass(string $class): string {
if (Requires::isDangerous($class)) {
throw new \Exception("Dangerious URI!");
}
return preg_replace('/[^a-zA-Z0-9_]/', '', $class);
}
/**
* Filter possible unsafe URI, prevent ../up-level hackers
* @param string $uri
* @return string Safe URI
*/
public static function filterUri(string $uri): string {
if (Requires::isDangerous($uri) === true) {
throw new \Exception("Dangerious URI!");
}
return Requires::filterFileName($uri);
}
public static function idHash(): string {
return crc32($_SESSION['user_id']);
}
public static function isPrivateOrLocalIPSimple(string $ip): bool {
if (! self::getValidIp($ip)) {
return false; // Invalid
}
return (
$ip === '::1' || // IPv6 localhost
preg_match('/^127\./', $ip) || // IPv4 localhost
preg_match('/^10\./', $ip) || // 10.0.0.0/8
preg_match('/^172\.(1[6-9]|2[0-9]|3[0-1])\./', $ip) || // 172.16.0.0/12
preg_match('/^192\.168\./', $ip) || // 192.168.0.0/16
preg_match('/^fd[0-9a-f]{2}:/i', $ip) // IPv6 ULA (fc00::/7)
);
}
/**
* Filter IP return good IP or False!
* @param string $ip
* @return string | false
*/
public static function getValidIp(string $ip) {
return (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4|FILTER_FLAG_IPV6));
}
public static function getValidPublicIp(string $ip) {
return (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4|FILTER_FLAG_IPV6|FILTER_FLAG_NO_PRIV_RANGE));
}
/**
* Is the server on the local test domain name
* @return bool SERVER Domain name is on whitelist
*/
public static function isServerNameOnDomainList(array $whitelist): bool {
if (!isset($_SERVER['SERVER_NAME'])) {
return false;
}
return (in_array($_SERVER['SERVER_NAME'], $whitelist));
}
/**
* Check if same Domain as Server
* @return bool
*/
public static function requestIsSameDomain(): bool {
if (!isset($_SERVER['HTTP_REFERER'])) {
// No referer send, so can't be same domain!
return false;
} else {
$refererHost = parse_url($_SERVER['HTTP_REFERER'] . PHP_URL_HOST);
if ($refererHost === false) {
return false; // Malformed URL
}
$refed_host = $refererHost['host'] ?? "";
$server_host = $_SERVER['HTTP_HOST'];
return ($refed_host === $server_host);
}
}
public static function safeForEval(string $s): string {
//new line check
$nl = chr(10);
if (strpos($s, $nl)) {
throw new \Exception("String CR/LF not permitted");
}
$meta = ['$','{','}','[',']','`',';'];
$escaped = ['&#36','&#123','&#125','&#91','&#96','&#59'];
// add slashed for quotes and blackslashes
$out = addslashes($s);
// replace php meta chrs
$out = str_repeat($meta, $escaped, $out);
return $out;
}
public static function getClientIpAddress() {
$ipaddress = '';
if (isset($_SERVER['HTTP_CLIENT_IP'])) {
$ipaddress = $_SERVER['HTTP_CLIENT_IP'];
} else if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else if (isset($_SERVER['HTTP_X_FORWARDED'])) {
$ipaddress = $_SERVER['HTTP_X_FORWARDED'];
} else if (isset($_SERVER['HTTP_FORWARDED_FOR'])) {
$ipaddress = $_SERVER['HTTP_FORWARDED_FOR'];
} else if (isset($_SERVER['HTTP_FORWARDED'])) {
$ipaddress = $_SERVER['HTTP_FORWARDED'];
} else if (isset($_SERVER['REMOTE_ADDR'])) {
$ipaddress = $_SERVER['REMOTE_ADDR'];
} else {
$ipaddress = 'UNKNOWN';
}
return $ipaddress;
}
/**
* Make sure uploads (LIKE Images, etc...) do NOT run PHP code!!
* Checks for PHP tags inside of file.
* @param string $file
* @return bool true if PHP was found
*/
public static function fileContainsPhp(string $file): bool {
$file_handle = fopen($file, "r");
while (!feof($file_handle)) {
$line = fgets($file_handle);
$pos = strpos($line, '<?php');
if ($pos !== false) {
fclose($file_handle);
return true;
}
}
fclose($file_handle);
return false;
}
}

@ -0,0 +1,66 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\String;
use IOcornerstone\Framework\Exception\BadMethodCallException;
/**
* Description of StringFacade
*
* @author Robert Strutts
*/
class StringFacade
{
private static ?bool $mbDefault = null;
/**
* Set the default multibyte behavior.
* Pass `true` to force multibyte, `false` to force single-byte, or `null` to auto-detect.
*/
public static function setMultibyteDefault(?bool $flag): void {
self::$mbDefault = $flag;
}
protected static function isMultibyte($str) {
// Use override if set
if (self::$mbDefault !== null) {
return self::$mbDefault;
}
$enabled = extension_loaded('mbstring');
if ($enabled === false) {
return false;
}
return mb_detect_encoding($str, mb_detect_order(), true) !== false &&
preg_match('/[^\x00-\x7F]/', $str);
}
protected static function getClass($str) {
return self::isMultibyte($str) ? 'mbStringFns' : 'StringFns';
}
public static function getFn($method, ...$args) {
if (empty($args)) {
throw new InvalidArgumentException("At least one argument is required for multibyte check.");
}
$class = "\\IOcornerstone\\Framework\\String\\" . self::getClass($args[0]);
if (!method_exists($class, $method)) {
throw new BadMethodCallException("Method $method does not exist in class $class.");
}
return call_user_func_array([$class, $method], $args);
}
// Optional: Static passthrough
public static function __callStatic($method, $args) {
return self::getFn($method, ...$args);
}
}

@ -0,0 +1,208 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\String;
/**
* Description of StringFns
*
* @author Robert Strutts
*/
class StringFns
{
public static function isUTF8(string $string) {
// Empty string is valid UTF-8
if ($string === '') {
return true;
}
// Convert string to array of bytes
$bytes = unpack('C*', $string);
// Pattern matching state
$state = 0;
$expectedBytes = 0;
foreach ($bytes as $byte) {
// Single byte character (0xxxxxxx)
if ($byte <= 0x7F) {
$state = 0;
continue;
}
// Start of multibyte sequence
if ($state === 0) {
// 2 bytes (110xxxxx)
if (($byte & 0xE0) === 0xC0) {
$expectedBytes = 1;
}
// 3 bytes (1110xxxx)
elseif (($byte & 0xF0) === 0xE0) {
$expectedBytes = 2;
}
// 4 bytes (11110xxx)
elseif (($byte & 0xF8) === 0xF0) {
$expectedBytes = 3;
}
// Invalid UTF-8 start byte
else {
return false;
}
$state = $expectedBytes;
continue;
}
// Continuation byte (10xxxxxx)
if (($byte & 0xC0) !== 0x80) {
return false;
}
$state--;
}
// Check if we finished the last multibyte sequence
return $state === 0;
}
// Check if string contains multibyte characters
public static function hasMultibyteChars(string $string) {
return (bool) preg_match('/[\xC2-\xDF][\x80-\xBF]|\xE0[\xA0-\xBF][\x80-\xBF]|[\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}|\xED[\x80-\x9F][\x80-\xBF]|\xF0[\x90-\xBF][\x80-\xBF]{2}|[\xF1-\xF3][\x80-\xBF]{3}|\xF4[\x80-\x8F][\x80-\xBF]{2}/', $string);
}
public static function strContains(string $haystack, string $needle) {
return $needle !== '' && strpos($haystack, $needle) !== false;
}
// Return character by Unicode code point value
public static function chr(int $codepoint): string|false {
return chr($codepoint);
}
// Parse GET/POST/COOKIE data and set global variable
public static function parseStr(string $string, array &$result): bool {
return parse_str($string, $result);
}
// Strip whitespace (or other characters) from the end of a string
public static function rtrim(string $string, ?string $characters = " \n\r\t\v\x00"): string {
return rtrim($string, $characters);
}
// Strip whitespace (or other characters) from the beginning of a string
public static function ltrim(string $string, ?string $characters = " \n\r\t\v\x00"): string {
return ltrim($string, $characters);
}
// Finds position of first occurrence of a string within another, case insensitive
public static function stripos(
string $haystack,
string $needle,
int $offset = 0
): int|false {
return stripos($haystack, $needle, $offset);
}
// Finds first occurrence of a string within another, case insensitive
public static function stristr(
string $haystack,
string $needle,
bool $before_needle = false
): string|false {
return stristr($haystack, $needle, $before_needle);
}
// Find position of first occurrence of string in a string
public static function strpos(
string $haystack,
string $needle,
int $offset = 0
): int|false {
return strpos($haystack, $needle, $offset);
}
// Finds the last occurrence of a character in a string within another
public static function strrchr(
string $haystack,
string $needle,
bool $before_needle = false,
): string|false {
return strrchr($haystack, $needle, $before_needle);
}
// Finds the last occurrence of a character in a string within another, case insensitive
public static function sstrrichr(
string $haystack,
string $needle,
bool $before_needle = false,
): string|false {
return strrichr($haystack, $needle, $before_needle);
}
// Finds position of last occurrence of a string within another, case insensitive
public static function strripos(
string $haystack,
string $needle,
int $offset = 0,
): int|false {
return strripos($haystack, $needle, $offset);
}
// Find position of last occurrence of a string in a string
public static function strrpos(
string $haystack,
string $needle,
int $offset = 0,
): int|false {
return strrpos($haystack, $needle, $offset);
}
// Finds first occurrence of a string within another
public static function strstr(
string $haystack,
string $needle,
bool $before_needle = false,
): string|false {
return strstr($haystack, $needle, $before_needle);
}
// Strip whitespace (or other characters) from the beginning and end of a string
public static function trim(string $string, ?string $characters = " \n\r\t\v\x00"): string {
return trim($string, $characters);
}
// Make a string's first character uppercase
public static function ucfirst(string $string): string {
return ucfirst($string);
}
// Override to get the length of a string with multibyte support
public static function strlen($string) {
return strlen($string);
}
// Override to convert a string to lowercase with multibyte support
public static function strtolower($string) {
return strtolower($string);
}
// Override to convert a string to uppercase with multibyte support
public static function strtoupper($string) {
return strtoupper($string);
}
// Override to get part/substring from a string with multibyte support
public static function substr($string, $start, $length = null) {
return ($length !== null) ? substr($string, $start, $length) : substr($string, $start);
}
// Count the number of substring occurrences
public static function substrCount(string $haystack, string $needle): int {
return substr_count($haystack, $needle);
}
}

@ -0,0 +1,167 @@
<?php
declare(strict_types = 1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\String;
/**
* Description of mbStringFns
*
* @author Robert Strutts
*/
class mbStringFns
{
public static function isUTF8(string $string) {
return mb_check_encoding($string, 'UTF-8');
}
// Check if string contains multibyte characters
public static function hasMultibyteChars(string $string) {
return strlen($string) !== mb_strlen($string, 'UTF-8');
}
public static function strContains(string $haystack, string $needle) {
return $needle !== '' && mb_strpos($haystack, $needle) !== false;
}
// Return character by Unicode code point value
public static function chr(int $codepoint, ?string $encoding = null): string|false {
return mb_chr($codepoint, $encoding);
}
// Parse GET/POST/COOKIE data and set global variable
public static function parseStr(string $string, array &$result): bool {
return mb_parse_str($string, $result);
}
// Strip whitespace (or other characters) from the end of a string
public static function rtrim(string $string, ?string $characters = null, ?string $encoding = null): string {
return mb_rtrim($string, $characters, $encoding);
}
// Strip whitespace (or other characters) from the beginning of a string
public static function ltrim(string $string, ?string $characters = null, ?string $encoding = null): string {
return mb_ltrim($string, $characters, $encoding);
}
// Finds position of first occurrence of a string within another, case insensitive
public static function stripos(
string $haystack,
string $needle,
int $offset = 0,
?string $encoding = null
): int|false {
return mb_stripos($haystack, $needle, $offset, $encoding);
}
// Finds first occurrence of a string within another, case insensitive
public static function stristr(
string $haystack,
string $needle,
bool $before_needle = false,
?string $encoding = null
): string|false {
return mb_stristr($haystack, $needle, $before_needle, $encoding);
}
// Find position of first occurrence of string in a string
public static function strpos(
string $haystack,
string $needle,
int $offset = 0,
?string $encoding = null
): int|false {
return mb_strpos($haystack, $needle, $offset, $encoding);
}
// Finds the last occurrence of a character in a string within another
public static function strrchr(
string $haystack,
string $needle,
bool $before_needle = false,
?string $encoding = null
): string|false {
return mb_strrchr($haystack, $needle, $before_needle, $encoding);
}
// Finds the last occurrence of a character in a string within another, case insensitive
public static function strrichr(
string $haystack,
string $needle,
bool $before_needle = false,
?string $encoding = null
): string|false {
return mb_strrichr($haystack, $needle, $before_needle, $encoding);
}
// Finds position of last occurrence of a string within another, case insensitive
public static function strripos(
string $haystack,
string $needle,
int $offset = 0,
?string $encoding = null
): int|false {
return mb_strripos($haystack, $needle, $offset, $encoding);
}
// Find position of last occurrence of a string in a string
public static function strrpos(
string $haystack,
string $needle,
int $offset = 0,
?string $encoding = null
): int|false {
return mb_strrpos($haystack, $needle, $offset, $encoding);
}
// Finds first occurrence of a string within another
public static function strstr(
string $haystack,
string $needle,
bool $before_needle = false,
?string $encoding = null
): string|false {
return mb_strstr($haystack, $needle, $before_needle, $encoding);
}
// Strip whitespace (or other characters) from the beginning and end of a string
public static function trim(string $string, ?string $characters = null, ?string $encoding = null): string {
return mb_trim($string, $characters, $encoding);
}
// Make a string's first character uppercase
public static function ucfirst(string $string, ?string $encoding = null): string {
return mb_ucfirst($string, $encoding);
}
// Override to get the length of a string with multibyte support
public static function strlen($string) {
return mb_strlen($string);
}
// Override to convert a string to lowercase with multibyte support
public static function strtolower($string) {
return mb_strtolower($string);
}
// Override to convert a string to uppercase with multibyte support
public static function strtoupper($string) {
return mb_strtoupper($string);
}
// Override to get part/substring from a string with multibyte support
public static function substr($string, $start, $length = null) {
return ($length !== null) ? mb_substr($string, $start, $length) : mb_substr($string, $start);
}
// Count the number of substring occurrences
public static function substrCount(string $haystack, string $needle, ?string $encoding = null): int {
return mb_substr_count($haystack, $needle, $encoding);
}
}

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
/*
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Trait\Security;
use IOcornerstone\Framework\Configure;
trait CsrfTokenFunctions
{
/**
* Get an Cross-Site Request Forge - Prevention Token
* @return string
*/
public static function csrfToken(): string {
return self::get_unique_id();
}
/**
* Set Session to use CSRF Token
* @return string CSRF Token
*/
public static function createCsrfToken(): string {
$token = self::csrfToken();
$_SESSION['csrf_token'] = $token;
$_SESSION['csrf_token_time'] = time();
return $token;
}
/**
* Destroy CSRF Token from Session
* @return bool success
*/
public static function destroyCsrfToken(): bool {
$_SESSION['csrf_token'] = null;
$_SESSION['csrf_token_time'] = null;
return true;
}
/**
* Get CSRF Token for use with HTML Form
* @return string Hidden Form with token set
*/
public static function csrfTokenTag(): string {
$token = self::createCsrfToken();
return "<input type=\"hidden\" name=\"csrf_token\" value=\"" . $token . "\">";
}
/**
* Check if POST data CSRF Token is Valid
* @return bool is valid
*/
public static function csrfTokenIsValid(int $filter = FILTER_UNSAFE_RAW): bool {
$isCsrf = filter_has_var(INPUT_POST, 'csrf_token');
if ($isCsrf) {
$userToken = filter_input(INPUT_POST, 'csrf_token', $filter);
$storedToken = $_SESSION['csrf_token'] ?? '';
if (empty($storedToken)) {
return false;
}
return ($user_token === $stored_token);
} else {
return false;
}
}
/**
* Optional check to see if token is also recent
* @return bool
*/
public static function csrfTokenIsRecent(): bool {
$max_elapsed = intval(Configure::get(
'security',
'max_token_age'
));
if ($max_elapsed < 30) {
$max_elapsed = 60 * 60 * 24; // 1 day
}
if (isset($_SESSION['csrf_token_time'])) {
$stored_time = $_SESSION['csrf_token_time'];
return ($stored_time + $max_elapsed) >= time();
} else {
// Remove expired token
self::destroyCsrfToken();
return false;
}
}
}

@ -0,0 +1,161 @@
<?php
declare(strict_types=1);
/*
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Trait\Security;
use IOcornerstone\Framework\Registry;
/**
*
* @author Robert Strutts
*/
trait SessionHijackingFunctions
{
public static function initSessions() {
if (Registry::get('di')->has('sessions')) {
Registry::get('di')->get_service('sessions');
}
}
// Function to forcibly end the session
public static function endSession() {
// Use both for compatibility with all browsers
// and all versions of PHP.
session_unset();
session_destroy();
}
// Does the request IP match the stored value?
public static function requestIpMatchesSession() {
// return false if either value is not set
if (!isset($_SESSION['ip']) || !isset($_SERVER['REMOTE_ADDR'])) {
return false;
}
if ($_SESSION['ip'] === $_SERVER['REMOTE_ADDR']) {
return true;
} else {
return false;
}
}
// Does the request user agent match the stored value?
public static function requestUserAgentMatchesSession() {
// return false if either value is not set
if (!isset($_SESSION['user_agent']) || !isset($_SERVER['HTTP_USER_AGENT'])) {
return false;
}
if ($_SESSION['user_agent'] === $_SERVER['HTTP_USER_AGENT']) {
return true;
} else {
return false;
}
}
// Has too much time passed since the last login?
public static function lastLoginIsRecent() {
$max_elapsed = intval(Configure::get(
'security',
'max_last_login_age'
));
if ($max_elapsed < 30) {
$max_elapsed = 60 * 60 * 24; // 1 day
}
// return false if value is not set
if (!isset($_SESSION['last_login'])) {
return false;
}
if (($_SESSION['last_login'] + $max_elapsed) >= time()) {
return true;
} else {
return false;
}
}
// Should the session be considered valid?
public static function isSessionValid() {
$check_ip = true;
$check_user_agent = true;
$check_last_login = true;
if ($check_ip && !self::requestIpMatchesSession()) {
return false;
}
if ($check_user_agent && !self::requestUserAgentMatchesSession()) {
return false;
}
if ($check_last_login && !self::lastLoginIsRecent()) {
return false;
}
return true;
}
// If session is not valid, end and redirect to login page.
public static function confirmSessionIsValid(
string $login = "login.php"
) {
if (!self::isSessionValid()) {
self::endSession();
// Note that header redirection requires output buffering
// to be turned on or requires nothing has been output
// (not even whitespace).
header("Location: " . $login );
exit;
}
}
// Is user logged in already?
public static function isLoggedIn() {
return (isset($_SESSION['logged_in']) && $_SESSION['logged_in']);
}
// If user is not logged in, end and redirect to login page.
public static function confirmUserLoggedIn(
string $login = "login.php"
) {
if (!self::isLoggedIn()) {
self::endSession();
// Note that header redirection requires output buffering
// to be turned on or requires nothing has been output
// (not even whitespace).
header("Location: " . $login);
exit;
}
}
// Actions to preform after every successful login
public static function afterSuccessfulLogin() {
// Regenerate session ID to invalidate the old one.
// Super important to prevent session hijacking/fixation.
session_regenerate_id();
$_SESSION['logged_in'] = true;
// Save these values in the session, even when checks aren't enabled
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
$_SESSION['last_login'] = time();
}
// Actions to preform after every successful logout
public static function afterSuccessfulLogout() {
$_SESSION['logged_in'] = false;
self::endSession();
}
// Actions to preform before giving access to any
// access-restricted page.
public static function beforeEveryProtectedPage(
string $login = "login.php"
) {
self::confirmUserLoggedIn($login);
self::confirmSessionIsValid();
}
}

@ -0,0 +1,208 @@
<?php
declare(strict_types=1);
namespace IOcornerstone\Framework\Trait;
use Psr\Http\Message\{
ServerRequestInterface,
UriInterface,
StreamInterface,
UploadedFileInterface
};
trait ServerRequestDelegation
{
private string $requestTarget = '';
/* ---------- RequestInterface ---------- */
public function getRequestTarget(): string
{
if ($this->requestTarget !== '') {
return $this->requestTarget;
}
$target = $this->uri->getPath();
$query = $this->uri->getQuery();
return $query ? $target . '?' . $query : $target;
}
public function withRequestTarget($requestTarget): self
{
$clone = clone $this;
$clone->requestTarget = $requestTarget;
return $clone;
}
public function getMethod(): string
{
return $this->method;
}
public function withMethod($method): self
{
$clone = clone $this;
$clone->method = $method;
return $clone;
}
public function getUri(): UriInterface
{
return $this->uri;
}
public function withUri(UriInterface $uri, $preserveHost = false): self
{
$clone = clone $this;
$clone->uri = $uri;
return $clone;
}
/* ---------- MessageInterface ---------- */
public function getProtocolVersion(): string
{
return $this->protocol;
}
public function withProtocolVersion($version): self
{
$clone = clone $this;
$clone->protocol = $version;
return $clone;
}
public function getHeaders(): array
{
return $this->headers;
}
public function hasHeader($name): bool
{
return isset($this->headers[strtolower($name)]);
}
public function getHeader($name): array
{
return $this->headers[strtolower($name)] ?? [];
}
public function getHeaderLine($name): string
{
return implode(',', $this->getHeader($name));
}
public function withHeader($name, $value): self
{
$clone = clone $this;
$clone->headers[strtolower($name)] = (array) $value;
return $clone;
}
public function withAddedHeader($name, $value): self
{
$clone = clone $this;
$clone->headers[strtolower($name)][] = $value;
return $clone;
}
public function withoutHeader($name): self
{
$clone = clone $this;
unset($clone->headers[strtolower($name)]);
return $clone;
}
public function getBody(): StreamInterface
{
return $this->body;
}
public function withBody(StreamInterface $body): self
{
$clone = clone $this;
$clone->body = $body;
return $clone;
}
/* ---------- ServerRequestInterface ---------- */
public function getServerParams(): array
{
return $this->serverParams;
}
public function getCookieParams(): array
{
return $this->cookies;
}
public function withCookieParams(array $cookies): self
{
$clone = clone $this;
$clone->cookies = $cookies;
return $clone;
}
public function getQueryParams(): array
{
return $this->queryParams;
}
public function withQueryParams(array $query): self
{
$clone = clone $this;
$clone->queryParams = $query;
return $clone;
}
public function getParsedBody(): mixed
{
return $this->parsedBody;
}
public function withParsedBody($data): self
{
$clone = clone $this;
$clone->parsedBody = $data;
return $clone;
}
public function getUploadedFiles(): array
{
return $this->uploadedFiles;
}
public function withUploadedFiles(array $uploadedFiles): self
{
$clone = clone $this;
$clone->uploadedFiles = $uploadedFiles;
return $clone;
}
public function getAttributes(): array
{
return $this->attributes;
}
public function getAttribute($name, $default = null): mixed
{
return $this->attributes[$name] ?? $default;
}
public function withAttribute($name, $value): self
{
$clone = clone $this;
$clone->attributes[$name] = $value;
return $clone;
}
public function withoutAttribute($name): self
{
$clone = clone $this;
unset($clone->attributes[$name]);
return $clone;
}
}

@ -0,0 +1,22 @@
The MIT License
Copyright (c) 2010-2026 Robert Strutts
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace IOcornerstone;
/**
* @author http://php-fig.org/ <info@php-fig.org>
* @copyright Copyright (c) 2013-2017 PHP Framework Interop Group
* @license MIT
* @site https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader-examples.md
*/
final class Psr4AutoloaderClass {
/**
* An associative array where the key is a namespace prefix and the value
* is an array of base directories for classes in that namespace.
*
* @var array
*/
protected $prefixes = [];
protected $loadedFiles = [];
/**
* Register loader with SPL autoloader stack.
*
* @return void
*/
public function register() {
spl_autoload_register(array($this, 'loadClass'));
}
public function isLoaded(string $prefix): bool {
$prefix = trim($prefix, '\\') . '\\';
return (isset($this->prefixes[$prefix])) ? true : false;
}
public function getList(): array {
return $this->prefixes;
}
public function getFilesList(): array {
return $this->loadedFiles;
}
/**
* Adds a base directory for a namespace prefix.
*
* @param string $prefix The namespace prefix.
* @param string|array $base_dir A base directory for class files in the
* namespace.
* @param bool $prepend If true, prepend the base directory to the stack
* instead of appending it; this causes it to be searched first rather
* than last.
* @return void
*/
public function addNamespace(string $prefix, string|array $baseDir, bool $prepend = false): void {
$prefix = trim($prefix, '\\') . '\\';
// Normalize baseDir to always be an array
if (!is_array($baseDir)) {
$baseDir = [$baseDir];
}
// Normalize each directory path
$baseDir = array_map(function($dir) {
return rtrim($dir, DIRECTORY_SEPARATOR) . '/';
}, $baseDir);
// Initialize array if prefix doesn't exist
if (!isset($this->prefixes[$prefix])) {
$this->prefixes[$prefix] = [];
}
// Add directories
if ($prepend) {
// Merge and prepend new directories
$this->prefixes[$prefix] = array_merge($baseDir, $this->prefixes[$prefix]);
} else {
// Merge and append new directories
$this->prefixes[$prefix] = array_merge($this->prefixes[$prefix], $baseDir);
}
// Optional: Remove duplicates while preserving order
$this->prefixes[$prefix] = array_values(array_unique($this->prefixes[$prefix]));
}
/**
* Loads the class file for a given class name.
*
* @param string $class The fully-qualified class name.
* @return mixed The mapped file name on success, or boolean false on
* failure.
*/
protected function loadClass(string $class): false|string {
if (!strrpos($class, '\\')) {
$ret = $this->loadMappedFile($class . '\\', $class);
if ($ret !== false) {
return $ret;
}
}
$prefix = $class;
while (false !== $pos = strrpos($prefix, '\\')) {
// retain the trailing namespace separator in the prefix
$prefix = substr($class, 0, $pos + 1);
$relativeClass = substr($class, $pos + 1);
$mappedFile = $this->loadMappedFile($prefix, $relativeClass);
if ($mappedFile) {
return $mappedFile;
}
// remove the trailing namespace separator for the next iteration
$prefix = rtrim($prefix, '\\');
}
// never found a mapped file
return false;
}
/**
* Load the mapped file for a namespace prefix and relative class.
*
* @param string $prefix The namespace prefix
* @param string $relativeClass The relative class name
* @return false|string Boolean false if no mapped file can be loaded,
* or the name of the mapped file that was loaded
*/
protected function loadMappedFile(string $prefix, string $relativeClass): false|string {
// Check if there are any base directories for this namespace prefix
if (!isset($this->prefixes[$prefix])) {
return false;
}
// Iterate through all base directories for this prefix
foreach ($this->prefixes[$prefix] as $baseDir) {
// Replace namespace separators with directory separators
$file = str_replace('\\', '/', $relativeClass) . '.php';
// If the mapped file exists, require it
if ($this->requireFile($baseDir, $file)) {
return $file;
}
}
// None of the directories contained the file
return false;
}
/**
* If a file exists, require it from the file system.
*
* @param string $file The file to require.
* @return bool True if the file exists, false if not.
*/
private function requireFile(string $path, string $file): bool {
if ($file === "Framework/Requires.php") {
return true; // Already used...
}
$req_class = \IOcornerstone\Framework\Requires::class;
if (! method_exists($req_class, "saferFileExists")) {
require_once IO_CORNERSTONE_FRAMEWORK . DIRECTORY_SEPARATOR . "Framework" . DIRECTORY_SEPARATOR . "Requires.php";
}
$saferFile = $req_class::saferFileExists($file, $path);
if ($saferFile !== false) {
if (defined('CountFiles') && CountFiles) {
if (! isset($this->loadedFiles[$saferFile])) {
require $saferFile;
$this->loadedFiles[$saferFile] = true;
}
} else {
require_once $saferFile;
}
return true;
}
return false;
}
}

@ -0,0 +1,8 @@
{
"require": {
"psr/log": "^3.0",
"psr/container": "^2.0",
"psr/http-server-middleware": "^1.0",
"psr/http-message": "^2.0"
}
}

288
src/composer.lock generated

@ -0,0 +1,288 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "e1055315e743298ad7d8328c70772d6a",
"packages": [
{
"name": "psr/container",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
"shasum": ""
},
"require": {
"php": ">=7.4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/2.0.2"
},
"time": "2021-11-05T16:47:00+00:00"
},
{
"name": "psr/http-message",
"version": "2.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-message.git",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Message\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP messages",
"homepage": "https://github.com/php-fig/http-message",
"keywords": [
"http",
"http-message",
"psr",
"psr-7",
"request",
"response"
],
"support": {
"source": "https://github.com/php-fig/http-message/tree/2.0"
},
"time": "2023-04-04T09:54:51+00:00"
},
{
"name": "psr/http-server-handler",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-server-handler.git",
"reference": "84c4fb66179be4caaf8e97bd239203245302e7d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4",
"reference": "84c4fb66179be4caaf8e97bd239203245302e7d4",
"shasum": ""
},
"require": {
"php": ">=7.0",
"psr/http-message": "^1.0 || ^2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP server-side request handler",
"keywords": [
"handler",
"http",
"http-interop",
"psr",
"psr-15",
"psr-7",
"request",
"response",
"server"
],
"support": {
"source": "https://github.com/php-fig/http-server-handler/tree/1.0.2"
},
"time": "2023-04-10T20:06:20+00:00"
},
{
"name": "psr/http-server-middleware",
"version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/http-server-middleware.git",
"reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
"reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829",
"shasum": ""
},
"require": {
"php": ">=7.0",
"psr/http-message": "^1.0 || ^2.0",
"psr/http-server-handler": "^1.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Http\\Server\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for HTTP server-side middleware",
"keywords": [
"http",
"http-interop",
"middleware",
"psr",
"psr-15",
"psr-7",
"request",
"response"
],
"support": {
"issues": "https://github.com/php-fig/http-server-middleware/issues",
"source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2"
},
"time": "2023-04-11T06:14:47+00:00"
},
{
"name": "psr/log",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
"shasum": ""
},
"require": {
"php": ">=8.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
],
"support": {
"source": "https://github.com/php-fig/log/tree/3.0.2"
},
"time": "2024-09-11T13:17:53+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}
Loading…
Cancel
Save