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