commit
951ac7ee20
@ -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 = ['$','{','}','[','`',';']; |
||||
// 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" |
||||
} |
||||
} |
||||
@ -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…
Reference in new issue