Robert 4 days ago
parent 9b2d9f9390
commit 14b5a48bed
  1. 2
      src/Bootstrap.php
  2. 34
      src/Framework/Assets.php
  3. 5
      src/Framework/Common.php
  4. 56
      src/Framework/Enum/FieldFilter.php
  5. 25
      src/Framework/Enum/Flags.php
  6. 40
      src/Framework/ErrorHandler.php
  7. 69
      src/Framework/HtmlDocument.php
  8. 10
      src/Framework/Http/App/App.php
  9. 1
      src/Framework/Http/Kernel.php
  10. 5
      src/Framework/Http/Request.php
  11. 2
      src/Framework/LoadAll.php
  12. 14
      src/Framework/Middleware/ErrorMiddleware.php
  13. 2
      src/Framework/ParagonCrypto/Crypto.php
  14. 6
      src/Framework/ParagonCrypto/PasswordStorage.php
  15. 4
      src/Framework/ParagonCrypto/SodiumStorage.php
  16. 109
      src/Framework/Playground/HttpContainer.php
  17. 79
      src/Framework/Playground/JunkForNow.php
  18. 77
      src/Framework/Playground/RouteServiceProvider.php
  19. 492
      src/Framework/Playground/Router.php
  20. 46
      src/Framework/Playground/ServiceProvider.php
  21. 32
      src/Framework/Playground/pipeOp.php
  22. 156
      src/Framework/SaferOutput.php
  23. 155
      src/Framework/Security.php
  24. 8
      src/Framework/Services/Sessions/CookieSessionHandler.php
  25. 141
      src/Framework/Trait/Security/CsrfTokenFunctions.php
  26. 199
      src/Framework/Trait/Security/SessionHijackingFunctions.php
  27. 6
      src/Framework/View.php
  28. 355
      src/Psr4AutoloaderClass.php

@ -59,7 +59,7 @@ function dump($var = 'nothing', endDump $end = endDump::KEEP_WORKING)
Common::dump($var, $end);
}
$debug = true; // <------------------- make false in production
$debug = false; // <------------------- make false in production
$myErrorHandler = new ErrorHandler($debug);
$myErrorHandler->register();

@ -14,8 +14,11 @@ use IOcornerstone\Framework\{
Security,
Common,
String\StringFacade,
Http\HttpFactory,
};
use Psr\Http\Message\ResponseInterface;
class Assets
{
private static $files = [];
@ -278,30 +281,29 @@ class Assets
return (file_exists($safe_path . $safe_file)) ? $safe_file : false;
}
/**
* meta redirect when headers are already sent...
* @param string url - site to do redirect on
* @reval none
*/
public static function gotoUrl(string $url): void
{
echo '<META http-equiv="refresh" content="0;URL=' . $url . '">';
exit;
}
/**
* Rediect to url and attempt to send via header.
* @param string $url - site to do redirect for
* @retval none - exits once done
* @param HttpFactory or $this->html...in controllers...
* @return ResponseInterface for Controllers to return...
*/
public static function redirectUrl(string $url): void
public static function redirectUrl(string $url, HttpFactory $http): ResponseInterface
{
$url = str_replace(array('&amp;', "\n", "\r"), array('&', '', ''), $url);
if (!headers_sent()) {
header('Location: ' . $url);
return $http->createResponse(403, ['Location' => $url], "");
} else {
self::goto_url($url);
return $http->returnResponse(403, [], self::gotoUrl($url));
}
exit;
}
/**
* meta redirect when headers are already sent...
* @param string url - site to do redirect on
*/
private static function gotoUrl(string $url): string
{
return '<META http-equiv="refresh" content="0;URL=' . $url . '">';
}
}

@ -26,6 +26,11 @@ final class Common
{
return (is_array($i) || is_object($i)) ? count($i) : 0;
}
public static function hasKeys(array $array): bool
{
return array_keys($array) !== range(0, count($array) - 1);
}
public static function stringSubPart(string $string, int $offset = 0, ?int $length = null, $encoding = null) {
if ($length === null) {

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Enum;
/**
*
* @author Robert Strutts
*/
enum FieldFilter: string {
case raw_string = "string";
case array_of_strings = "strings";
case email = "email-address";
case url = "site-url";
case raw = "unfiltered-non-sanitized";
case integer_number = "integer";
case array_of_ints = "integers";
case floating_point = "float";
case array_of_floats = "floats";
public function resolve() {
return match($this) {
self::raw_string => FILTER_UNSAFE_RAW,
self::array_of_strings => [
'filter' => FILTER_UNSAFE_RAW,
'flags' => FILTER_REQUIRE_ARRAY
],
self::email => FILTER_SANITIZE_EMAIL,
self::url => FILTER_SANITIZE_URL,
self::raw => FILTER_DEFAULT, // Unfiltered, non-sanitized!!!
self::integer_number => [
'filter' => FILTER_SANITIZE_NUMBER_INT,
'flags' => FILTER_REQUIRE_SCALAR
],
self::array_of_ints => [
'filter' => FILTER_SANITIZE_NUMBER_INT,
'flags' => FILTER_REQUIRE_ARRAY
],
self::floating_point => [
'filter' => FILTER_SANITIZE_NUMBER_FLOAT,
'flags' => FILTER_FLAG_ALLOW_FRACTION
],
self::array_of_floats => [
'filter' => FILTER_SANITIZE_NUMBER_FLOAT,
'flags' => FILTER_REQUIRE_ARRAY
],
};
}
}

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/*
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework\Enum;
/**
*
* @author Robert Strutts
*/
enum Flags
{
case RAW;
case TRIM;
case HTML_STIP_TAGS;
case HTML_ESCAPE;
case HTML_PURIFY;
CASE JSON_ENCODE;
}

@ -109,7 +109,7 @@ final class ErrorHandler
if ($this->debug) {
$this->renderConsole($e);
} else {
$this->renderProductionConsole();
$this->renderProductionConsole($e);
$this->logException($e);
}
return true;
@ -122,7 +122,7 @@ final class ErrorHandler
if ($this->debug) {
$this->renderDebug($e);
} else {
$this->renderProduction();
$this->renderProduction($e);
$this->logException($e);
}
// Don't execute PHP's internal error handler
@ -247,9 +247,17 @@ final class ErrorHandler
public function getJsonDebug(Throwable $e): string
{
$this->setJsonHeaders();
$dCode = $e->getCode() ?? 0;
if ($dCode > 0) {
$aCode = ['code' => $dCode];
} else {
$aCode = [];
}
return json_encode([
'error' => [
$aCode,
'type' => $this->getErrorType($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
@ -259,12 +267,13 @@ final class ErrorHandler
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
}
private function renderJsonProduction(): void
private function renderJsonProduction(Throwable $e): void
{
$this->setJsonHeaders();
echo json_encode([
'error' => [
'code' => $this->getCodeNumber($e),
'message' => 'Internal Server Error'
]
]);
@ -304,6 +313,12 @@ final class ErrorHandler
echo $color . $out . PHP_EOL;
}
public function getCodeNumber(Throwable $e): string
{
$dCode = $e->getCode() ?? 0;
return "(Code #{$dCode}); ";
}
public function formatWebMessage(Throwable $e): string
{
$styles = [
@ -315,8 +330,8 @@ final class ErrorHandler
$style = $styles[$type] ?? $styles['error'];
$content = htmlspecialchars((string) $e);
$message = wordwrap($content, WORD_WRAP_CHRS, "<br>\n");
$message = $this->getCodeNumber($e);
$message .= wordwrap($content, WORD_WRAP_CHRS, "<br>\n");
$assets = "/assets/uikit/css/uikit.gradient.min.css";
if (defined("BaseDir") &&
@ -332,21 +347,22 @@ final class ErrorHandler
return $msg;
}
public function getProdMessage(): string
public function getProdMessage(Throwable $e): string
{
if (Console::isConsole()) {
return $this->myErr;
return $this->getCodeNumber($e) . $this->myErr;
}
if ($this->isJsonRequest()) {
$this->setJsonHeaders();
return json_encode([
'error' => [
'code' => $this->getCodeNumber($e),
'message' => 'Internal Server Error'
]
]);
}
return "<h1 style=\"color: red;\">" . $this->myErr . "</h1>";
return "<h1 style=\"color: red;\">" . $this->getCodeNumber($e) . $this->myErr . "</h1>";
}
private function renderDebug(Throwable $e): void
@ -355,17 +371,17 @@ final class ErrorHandler
echo $this->formatWebMessage($e);
}
private function renderProductionConsole(): void
private function renderProductionConsole(Throwable $e): void
{
echo $this->myErr;
echo $this->getCodeNumber($e) . $this->myErr;
}
/**
* @todo Make error page red, etc...
*/
private function renderProduction(): void
private function renderProduction(Throwable $e): void
{
echo "<h1 style=\"color: red;\">" . $this->myErr . "</h1>";
echo "<h1 style=\"color: red;\">" . $this->getCodeNumber($e) . $this->myErr . "</h1>";
}
private function setLoggerByLevel(Throwable $e): void

@ -116,6 +116,16 @@ class HtmlDocument
$this->author = $author;
}
public function addToTitle(string $title): void
{
$this->title .= $title;
}
public function beforeTitle(string $title): void
{
$this->title = $title . $this->title;
}
/**
* Set Title for HTML
* @param string $title
@ -199,22 +209,33 @@ class HtmlDocument
$this->breadcrumb = $crumbs;
}
public function setAssetsFromArray(array $files, string $which, string $scope = 'project'): void
{
foreach ($files as $file => $a) {
switch ($which) {
case 'main_css':
$this->addMainCss($file, $scope, $a);
break;
case 'css':
$this->addCss($file, $scope, $a);
break;
case 'main_js':
$this->addMainJs($file, $scope, $a);
break;
case 'js':
$this->addJs($file, $scope, $a);
break;
private function doAction(string $file, string $which, string $scope, array $a): void
{
switch ($which) {
case 'main_css':
$this->addMainCss($file, $scope, $a);
break;
case 'css':
$this->addCss($file, $scope, $a);
break;
case 'main_js':
$this->addMainJs($file, $scope, $a);
break;
case 'js':
$this->addJs($file, $scope, $a);
break;
}
}
public function setAssetsFromArray(array $files, string $which, string $scope = 'project', array $options = []): void
{
if (Common::hasKeys($files)) {
foreach ($files as $file => $a) {
$this->doAction($file, $which, $scope, $a);
}
} else {
foreach ($files as $file) {
$this->doAction($file, $which, $scope, $options);
}
}
}
@ -476,18 +497,22 @@ class HtmlDocument
return $this->head;
}
public function getBreadcrumbsAuto(): string
public function getBreadcrumbsAuto(bool $showHomeSVG = false): string
{
if (! count($this->breadcrumb) && empty($this->activeCrumb)) {
if (!count($this->breadcrumb) && empty($this->activeCrumb)) {
return "";
}
$out = "<nav><ul class=\"breadcrumb\">" . PHP_EOL;
foreach($this->breadcrumb as $link => $crumb) {
if ($showHomeSVG) {
$out .= '<svg class="homecrumb" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"></path></svg>';
}
foreach ($this->breadcrumb as $link => $crumb) {
$out .= "<li><a href=\"$link\">$crumb</a></li>" . PHP_EOL;
}
if (! empty($this->activeCrumb)) {
if (!empty($this->activeCrumb)) {
$out .= "<li>" . $this->activeCrumb . "</li>" . PHP_EOL;
}
$out .= "</ul></nav>" . PHP_EOL;

@ -22,7 +22,8 @@ use IOcornerstone\Framework\{
Configure,
Common,
Console,
Security
Security,
View,
};
use Exception;
@ -111,7 +112,7 @@ class App implements MiddlewareAwareInterface
private function getCtrlDir(): string
{
$ctrl = (Console::isConsole()) ? "cli_" : "";
$ctrl = (Console::isConsole()) ? "CLI_" : "";
return ($this->testing) ? "test_" : $ctrl;
}
@ -193,7 +194,10 @@ class App implements MiddlewareAwareInterface
private function local404(): ResponseInterface
{
return (new HttpFactory())->createResponse(404, [], '404 Page - Not Found');
$view = new View();
$view->addView("OnError/404Page");
$myView = $view->fetch($this);
return (new HttpFactory())->createResponse(404, [], $myView);
}
/**

@ -124,6 +124,7 @@ class Kernel {
http_response_code($response->getStatusCode());
foreach ($response->getHeaders() as $name => $values) {
if (! is_array($values) && ! is_object($values)) {
header("$name: $values", false);
continue;
}
foreach ($values as $value) {

@ -29,6 +29,11 @@ final class Request implements ServerRequestInterface
private string $protocol = '1.1'
) {}
public function JSON_PostVar(): ParameterBag
{
return new ParameterBag((array) json_decode($this->getBody(), true));
}
/**
* Parameter Bags [has and get - methods]
*/

@ -24,7 +24,7 @@ final class LoadAll
self::doLoop($cdir, $config_path);
}
}
$service_path = $path . "Services";
$service_path = $path . "LoadServices";
if (is_dir($service_path)) {
$sdir = scandir($service_path);
if ($sdir !== false) {

@ -21,7 +21,7 @@ final class ErrorMiddleware implements MiddlewareInterface
{
public function __construct(
private LoggerInterface $logger,
private bool $displayErrors = false
private bool $hideErrors = false
) {}
public function process(
@ -31,12 +31,14 @@ final class ErrorMiddleware implements MiddlewareInterface
try {
return $handler->handle($request);
} catch (\Throwable $e) {
$codeNumber = Reg::get('error_handler')->getCodeNumber($e);
$message = $codeNumber . $e->getMessage();
$this->logger->error(
$e->getMessage(),
$message,
['exception' => $e]
);
$bodyString = $this->displayErrors
$bodyString = $this->hideErrors
? $this->formatException($e)
: 'Internal Server Error';
@ -55,12 +57,14 @@ final class ErrorMiddleware implements MiddlewareInterface
}
if ($live) {
return Reg::get('error_handler')->getProdMessage();
return Reg::get('error_handler')->getProdMessage($e);
}
if (Console::isConsole()) {
$codeNumber = $e->getCode() ?? 0;
return sprintf(
"%s\n\n%s",
"Code# %d; %s\n\n%s",
$codeNumber,
$e->getMessage(),
$e->getTraceAsString()
);

@ -53,7 +53,7 @@ class Crypto {
): string {
$key = $this->key;
$nonce = $this->rnd->get_bytes(
$nonce = $this->rnd->getBytes(
SODIUM_CRYPTO_SECRETBOX_NONCEBYTES
);
$fn = ($key_usage == self::singleKey) ? "sodium_crypto_secretbox" : "sodium_crypto_box";

@ -21,7 +21,7 @@ class PasswordStorage {
}
public function generateKey(): string {
return sodium_bin2hex($this->random_engine->get_bytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES));
return sodium_bin2hex($this->random_engine->getBytes(SODIUM_CRYPTO_SECRETBOX_KEYBYTES));
}
/**
@ -38,11 +38,11 @@ class PasswordStorage {
SODIUM_CRYPTO_PWHASH_SCRYPTSALSA208SHA256_OPSLIMIT_INTERACTIVE,
SODIUM_CRYPTO_PWHASH_SCRYPTSALSA208SHA256_MEMLIMIT_INTERACTIVE
);
$salt = $this->random_engine->get_bytes(self::SALT_SIZE_IN_BYTES);
$salt = $this->random_engine->getBytes(self::SALT_SIZE_IN_BYTES);
list ($enc_key, $auth_key) = $this->splitKeys($secret_key, sodium_bin2hex($salt));
sodium_memzero($secret_key);
sodium_memzero($password);
$nonce = $this->random_engine->get_bytes(
$nonce = $this->random_engine->getBytes(
SODIUM_CRYPTO_STREAM_NONCEBYTES
);
$cipher_text = sodium_crypto_stream_xor(

@ -35,10 +35,10 @@ class SodiumStorage {
}
public function encrypt(#[\SensitiveParameter] string $plain_text, string $item_name = ""): string {
$nonce = $this->randomEngine->get_bytes(
$nonce = $this->randomEngine->getBytes(
SODIUM_CRYPTO_STREAM_NONCEBYTES
);
$salt = $this->randomEngine->get_bytes(self::SALT_SIZE_IN_BYTES);
$salt = $this->randomEngine->getBytes(self::SALT_SIZE_IN_BYTES);
list ($enc_key, $auth_key) = $this->splitKeys($item_name, sodium_bin2hex($salt));
$cipher_text = sodium_crypto_stream_xor(
$plain_text,

@ -1,109 +0,0 @@
<?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;
}
}

@ -1,79 +0,0 @@
<?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;
}
}

@ -1,77 +0,0 @@
<?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']);
}
});
}
}

@ -1,492 +0,0 @@
<?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];
}
}

@ -1,46 +0,0 @@
<?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')
);
});
}
}
*/

@ -1,32 +0,0 @@
<?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,156 @@
<?php
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\Enum\Flags;
/**
* Description of SaferOutput
*
* @author Robert Strutts
*/
class SaferOutput
{
public static function get(
string $input,
string $default = '',
array $flags = [Flags::TRIM, Flags::HTML_ESCAPE]
): string
{
$value = $input ?? $default;
if (!is_string($value)) {
return $default;
}
foreach ($flags as $flag) {
$value = match ($flag) {
Flags::TRIM =>
trim($value),
Flags::HTML_STIP_TAGS =>
strip_tags($value),
Flags::HTML_ESCAPE =>
self::h($value),
Flags::HTML_PURIFY =>
self::p($value),
Flags::JSON_ENCODE =>
self::j($value),
};
}
return $value;
}
public static function convertToUTF8(string $in_str): string
{
if (!extension_loaded('mbstring')) {
return $in_str;
}
$cur_encoding = mb_detect_encoding($in_str);
if ($cur_encoding == "UTF-8" && mb_check_encoding($in_str, "UTF-8")) {
return $in_str;
} else {
return mb_convert_encoding($in_str, 'UTF-8', $cur_encoding);
}
}
// Escape HTML output
public static function h(string $string): string
{
$utf8 = self::convertToUTF8($string);
return htmlspecialchars($utf8, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5, 'UTF-8');
}
// Reverse encode of HTML
public static function htmlDecode(string $string): string
{
return htmlspecialchars_decode($string, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML5);
}
/**
* @todo FIX ME to use IOConerstone....!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*/
// HTML Purify library
public static function p(string $string): string
{
$purifer = \main_tts\registry::get('di')->get_service('html_filter');
if (!$purifer->has_loaded()) {
$purifer->set_defaults();
}
return $purifer->purify($string);
}
// Escape JavaScript output
public static function j($input, int $levels_deep = 512): mixed
{
try {
return json_encode($input, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR, $levels_deep);
} catch (\JsonException $ex) {
return $ex;
}
}
public static function jsonDecode(string $string, bool $return_as_an_array = true, int $levels_deep = 512): mixed
{
try {
return json_decode($string, $return_as_an_array, $levels_deep, JSON_THROW_ON_ERROR);
} catch (\JsonException $ex) {
return $ex;
}
}
public static function hasJsonError($object): bool
{
return ($object instanceof \JsonException);
}
// Escape URL output
public static function u(string $string): string
{
return urlencode($string);
}
/*
* Encode HTML kindof... The problem with htmlentities() is that it is not
* very powerful, in fact, it does not escape single quotes, cannot detect
* the character set and does not validate HTML as well.
*/
public static function e(string $string): string
{
$utf8 = self::convertToUTF8($string);
return htmlentities($utf8, ENT_QUOTES, 'UTF-8');
}
public static function de(string $data): string
{
return html_entity_decode($data);
}
/**
* As PHP uses the underlying C functions for file system related operations,
* it may handle null bytes in a quite unexpected way.
* As null bytes denote the end of a string in C, strings
* containing them won't be considered entirely but rather
* only until a null byte occurs. So, clean it out to
* avoid vulnerable code.
*/
public static function removeNullByte(string $input): string
{
return str_replace(chr(0), '', $input);
}
}

@ -1,17 +1,19 @@
<?php
declare(strict_types = 1);
declare(strict_types=1);
/**
* @author Robert Strutts
* @copyright (c) 2026, Robert Strutts
* @license MIT
*/
namespace IOcornerstone\Framework;
use IOcornerstone\Framework\{
Configure,
Requires
Requires,
RandomEngine,
};
/**
@ -21,14 +23,32 @@ use IOcornerstone\Framework\{
*/
class Security
{
use \IOcornerstone\Framework\Trait\Security\CsrfTokenFunctions;
use \IOcornerstone\Framework\Trait\Security\SessionHijackingFunctions;
public static function hexPWD(int $bytes = 16): string
{
$r = new RandomEngine();
return sodium_bin2hex($r->getBytes($bytes));
}
public static function base64PWD(int $bytes = 16): string
{
$r = new RandomEngine();
return rtrim(strtr(
base64_encode($r->getBytes($bytes)),
'+/',
'-_'
), '=');
}
/**
* Get unique IDs for database
* @return int
*/
public static function getUniqueNumber(): int {
public static function getUniqueNumber(): int
{
return abs(crc32(microtime()));
}
@ -36,13 +56,15 @@ class Security
* Get token
* @return string
*/
public static function getUniqueId(): 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) {
public static function useHmac(string $algo, string $pepper)
{
if (!function_exists("hash_hmac_algos")) {
throw new \Exception("hash_hmac not installed!");
}
@ -72,8 +94,9 @@ class Security
* intentionally slower hashing algorithms such as bcrypt
* or Argon2 should be used.
*/
public static function findDefaultHashAlgo() {
public static function findDefaultHashAlgo()
{
if (defined("PASSWORD_ARGON2ID"))
return PASSWORD_ARGON2ID;
if (defined("PASSWORD_ARGON2"))
@ -84,35 +107,38 @@ class Security
return PASSWORD_BCRYPT;
return false;
}
private static function isValidHashAlgo($algo): bool {
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 {
public static function do_password_hash(#[\SensitiveParameter] string $password): bool|string
{
$pwdPeppered = self::makeHash($password);
$hashAlgo = Configure::get(
"security",
"hash_algo"
) ?? false;
"security",
"hash_algo"
) ?? false;
if ($hashAlgo === false) {
throw new \Exception("Security Hash Algo not set!");
}
if (! self::isValidHashAlgo($hashAlgo)) {
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 {
#[\SensitiveParameter] string $inputPwd, #[\SensitiveParameter] $dbPassword
): bool
{
$pwdPeppered = self::makeHash($inputPwd);
return password_verify($pwdPeppered, $dbPassword);
}
@ -123,7 +149,8 @@ class Security
* @param string $level (weak, low, high, max)
* @return string new Hashed
*/
public static function makeHash(#[\SensitiveParameter] string $text): string {
public static function makeHash(#[\SensitiveParameter] string $text): string
{
$level = Configure::get('security', 'hash_level');
if (empty($level)) {
$level = "normal";
@ -167,19 +194,20 @@ class Security
}
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);
/**
* @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);
}
/**
@ -187,48 +215,55 @@ class Security
* @param string $uri
* @return string Safe URI
*/
public static function filterUri(string $uri): string {
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 {
public static function idHash(): string
{
return crc32($_SESSION['user_id']);
}
public static function isPrivateOrLocalIPSimple(string $ip): bool {
if (! self::getValidIp($ip)) {
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)
$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 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));
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 {
public static function isServerNameOnDomainList(array $whitelist): bool
{
if (!isset($_SERVER['SERVER_NAME'])) {
return false;
}
@ -239,7 +274,8 @@ class Security
* Check if same Domain as Server
* @return bool
*/
public static function requestIsSameDomain(): bool {
public static function requestIsSameDomain(): bool
{
if (!isset($_SERVER['HTTP_REFERER'])) {
// No referer send, so can't be same domain!
return false;
@ -255,14 +291,15 @@ class Security
}
}
public static function safeForEval(string $s): string {
public static function safeForEval(string $s): string
{
//new line check
$nl = chr(10);
if (strpos($s, $nl)) {
throw new \Exception("String CR/LF not permitted");
}
$meta = ['$','{','}','[',']','`',';'];
$escaped = ['&#36','&#123','&#125','&#91','&#96','&#59'];
$meta = ['$', '{', '}', '[', ']', '`', ';'];
$escaped = ['&#36', '&#123', '&#125', '&#91', '&#96', '&#59'];
// add slashed for quotes and blackslashes
$out = addslashes($s);
// replace php meta chrs
@ -270,7 +307,8 @@ class Security
return $out;
}
public static function getClientIpAddress() {
public static function getClientIpAddress(): string
{
$ipaddress = '';
if (isset($_SERVER['HTTP_CLIENT_IP'])) {
$ipaddress = $_SERVER['HTTP_CLIENT_IP'];
@ -296,7 +334,8 @@ class Security
* @param string $file
* @return bool true if PHP was found
*/
public static function fileContainsPhp(string $file): bool {
public static function fileContainsPhp(string $file): bool
{
$file_handle = fopen($file, "r");
while (!feof($file_handle)) {
$line = fgets($file_handle);
@ -308,5 +347,5 @@ class Security
}
fclose($file_handle);
return false;
}
}
}

@ -6,6 +6,7 @@ namespace IOcornerstone\Framework\Services\Sessions;
use IOcornerstone\Framework\GzCompression;
use IOcornerstone\Framework\Enum\CompressionMethod as Method;
use IOcornerstone\ErrorCodes;
/**
* @author Robert Strutts
@ -93,7 +94,12 @@ class CookieSessionHandler implements \SessionHandlerInterface {
$data = $de; // data is now decrypted
}
}
$gd = $this->compression->decompress($data);
try {
$gd = $this->compression->decompress($data);
} catch (\Exception $ex) {
echo ErrorCodes::getErrorCodeFor(ErrorCodes::GZ_Inflate_Cookie_Data_Tampered_With, $ex, throwMe: true);
return "";
}
return ($gd !== false) ? $gd : $data;
}

@ -14,82 +14,133 @@ 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
* Useful for JSON data...
* @return string CSRF Token
*/
public static function createCsrfToken(): string {
$token = self::csrfToken();
$_SESSION['csrf_token'] = $token;
$_SESSION['csrf_token_time'] = time();
return $token;
public static function createCsrfToken(): string
{
self::setup();
$newToken = self::csrfToken();
$_SESSION['csrf_pool'][$newToken] = time();
return $newToken;
}
/**
* Destroy CSRF Token from Session
* @return bool success
* Keep only last 15 tokens to prevent memory issues.
*/
public static function destroyCsrfToken(): bool {
$_SESSION['csrf_token'] = null;
$_SESSION['csrf_token_time'] = null;
return true;
public static function cleanUp(){
$keepOnly = 15;
if (count($_SESSION['csrf_pool']) > $keepOnly) {
$_SESSION['csrf_pool'] = array_slice($_SESSION['csrf_pool'], -$keepOnly, null, true);
}
}
/**
* Get CSRF Token for use with HTML Form
* @return string Hidden Form with token set
*/
public static function csrfTokenTag(): string {
public static function csrfTokenTag(): string
{
$token = self::createCsrfToken();
return "<input type=\"hidden\" name=\"csrf_token\" value=\"" . $token . "\">";
return "<input type=\"hidden\" id=\"csrf_token\" 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 {
public static function csrfTokenStillValid(string $csrfTokenKeyName = ""): bool
{
if (empty($csrfTokenKeyName)) {
$csrfTokenKeyName = $_POST['csrf_token'] ?? "";
}
$validTimeStamp = self::csrfTokenIsValid($csrfTokenKeyName);
if ($validTimeStamp === false) {
return false;
}
$recent = self::csrfTokenIsRecent($validTimeStamp);
self::destroyCsrfToken($csrfTokenKeyName); // Done, so clean up Consume token
return $recent;
}
/**
* Optional check to see if token is also recent
* @return bool
* Get an Cross-Site Request Forge - Prevention Token
* @return string
*/
public static function csrfTokenIsRecent(): bool {
private static function csrfToken(): string
{
return self::getUniqueId();
}
private static function setup(): void
{
$clean_ts = intval(Configure::get(
'security',
'token_life'
));
if ($clean_ts < 60) {
$clean_ts = 3600;
}
if (!isset($_SESSION['csrf_pool'])) {
$_SESSION['csrf_pool'] = [];
}
// Clean old tokens by useage time token_life
foreach ($_SESSION['csrf_pool'] as $key => $timestamp) {
if ($timestamp < (time() - $clean_ts)) {
unset($_SESSION['csrf_pool'][$key]);
}
}
}
private static function csrfTokenIsValid(string $csrfTokenKeyName = ""): false|int
{
if (empty($csrfTokenKeyName)) {
$csrfTokenKeyName = $_POST['csrf_token'] ?? "";
}
if (!isset($_SESSION['csrf_pool'][$csrfTokenKeyName])) {
return false;
}
$tsToken = $_SESSION['csrf_pool'][$csrfTokenKeyName];
if (is_int($tsToken)){
return $tsToken;
}
return false;
}
private static function csrfTokenIsRecent($tokenTimeStored): bool
{
$max_elapsed = intval(Configure::get(
'security',
'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 ($tokenTimeStored + $max_elapsed) >= time();
}
/**
* Destroy CSRF Token from Session
* @return bool success
*/
private static function destroyCsrfToken(string $csrfTokenKeyName = ""): bool
{
if (empty($csrfTokenKeyName)) {
$csrfTokenKeyName = $_POST['csrf_token'] ?? "";
}
if (!isset($_SESSION['csrf_pool'][$csrfTokenKeyName])) {
return false;
}
unset($_SESSION['csrf_pool'][$csrfTokenKeyName]); // Consume token
return true;
}
}

@ -18,13 +18,18 @@ use IOcornerstone\Framework\Registry;
*/
trait SessionHijackingFunctions
{
/**
* Begin Sessions
*/
public static function initSessions() {
if (Registry::get('di')->has('sessions')) {
Registry::get('di')->get_service('sessions');
}
}
// Function to forcibly end the session
/**
* Function to forcibly end the session
*/
public static function endSession() {
// Use both for compatibility with all browsers
// and all versions of PHP.
@ -32,59 +37,18 @@ trait SessionHijackingFunctions
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;
/**
*
* @param bool $check_ip
* @param bool $check_user_agent
* @param bool $check_last_login
* @return bool Should the session be considered valid?
*/
public static function isSessionValid(
bool $check_ip = true,
bool $check_user_agent = true,
bool $check_last_login = true,
) {
if ($check_ip && !self::requestIpMatchesSession()) {
return false;
}
@ -97,28 +61,20 @@ trait SessionHijackingFunctions
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 bool Is user logged in already?
*/
public static function isLoggedIn(): bool {
return (isset($_SESSION['logged_in']) && $_SESSION['logged_in']);
}
// If user is not logged in, end and redirect to login page.
/**
* If user is not logged in, end and redirect to login page.
* @param string $login
*/
public static function confirmUserLoggedIn(
string $login = "login.php"
string $login = "/App/Home/Login.html"
) {
if (!self::isLoggedIn()) {
self::endSession();
@ -130,7 +86,9 @@ trait SessionHijackingFunctions
}
}
// Actions to preform after every successful login
/**
* 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.
@ -144,18 +102,103 @@ trait SessionHijackingFunctions
$_SESSION['last_login'] = time();
}
// Actions to preform after every successful logout
/**
* 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.
/**
* Actions to preform before giving access to any
* access-restricted page.
*/
public static function beforeEveryProtectedPage(
string $login = "login.php"
string $login = "/App/Home/Login.html",
array $options = []
) {
self::confirmUserLoggedIn($login);
self::confirmSessionIsValid();
self::confirmSessionIsValid($login, $options);
}
/**
* If session is not valid, end and redirect to login page.
* @param string $login
* @param array $options
*/
private static function confirmSessionIsValid(
string $login = "/App/Home/Login.html",
array $options = []
) {
if (!self::isSessionValid(
$options['check_ip'] ?? true,
$options['check_agent'] ?? true,
$options['check_is_recent_login'] ?? true
)) {
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;
}
}
/**
*
* @return bool Does the request IP match the stored value?
*/
private 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;
}
}
/**
*
* @return bool Does the request user agent match the stored value?
*/
private 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;
}
}
/**
*
* @return bool Has too much time passed since the last login?
*/
private 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;
}
}
}

@ -173,7 +173,7 @@ final class View
* @param mixed $name Name of the variable to set in the view, or an array of key/value pairs where each key is the variable and each value is the value to set.
* @param mixed $value Value of the variable to set in the view.
*/
public function set($name, mixed $value = null): void
public function set(array|string $name, mixed $value = null): void
{
if (is_array($name)) {
foreach ($name as $var_name => $value) {
@ -212,6 +212,10 @@ final class View
$local = $this; // FALL Back, please use fetch($this);
}
if (isset($local->html)) {
$this->vars['html'] = $local->html;
}
if ($this->useTemplateEngineLiquid) {
$this->tempalteEngineLiquid = Registry::get('di')->get_service('liquid', [$this->templateType]);
if ($this->whiteSpaceControl) {

@ -10,171 +10,212 @@ namespace IOcornerstone;
* @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];
final class Psr4AutoloaderClass
{
/*
* log, and sql do not contian PHP files, so they are fine...
* a correct NameSpace must be set in the files, so this is not a bug issue...
*/
private $blockListOfDirs = [
"vendor", // Stop Entry into Vendor folder
];
/**
* 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.
*/
public function register(): void
{
/**
* use [[[ var_dump ]]] in this file or enable dd & Dumps here...
*/
// $this->enableDumps();
spl_autoload_register(array($this, 'loadClass'));
}
// 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] = [];
public function enableDumps(): void
{
include IO_CORNERSTONE_FRAMEWORK . "Framework" . DIRECTORY_SEPARATOR . "Enum" . DIRECTORY_SEPARATOR . "ExitOnDump.php";
include IO_CORNERSTONE_FRAMEWORK . "Framework" . DIRECTORY_SEPARATOR . "Common.php";
include IO_CORNERSTONE_FRAMEWORK . "Framework" . DIRECTORY_SEPARATOR . "Configure.php";
}
// 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);
public function isLoaded(string $prefix): bool
{
$prefix = trim($prefix, '\\') . '\\';
return (isset($this->prefixes[$prefix])) ? true : false;
}
// 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;
}
public function getList(): array
{
return $this->prefixes;
}
/**
* 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;
}
public function getFilesList(): array
{
return $this->loadedFiles;
}
/**
* 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...
/**
* 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]));
}
$req_class = \IOcornerstone\Framework\Requires::class;
if (! method_exists($req_class, "saferFileExists")) {
require_once IO_CORNERSTONE_FRAMEWORK . DIRECTORY_SEPARATOR . "Framework" . DIRECTORY_SEPARATOR . "Requires.php";
/**
* 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;
}
$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;
/**
* 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);
/**
* Lock Down our NameSpaces More...
*/
if ($prefix === "IOcornerstone\\" || $prefix === "Project\\") {
foreach ($this->blockListOfDirs as $blockedDir) {
$sDir = "/" . $blockedDir . "/";
if (str_contains($file, $sDir)) {
return false; // Return false = Deny
}
}
}
$file .= ".php"; // Make sure we ONLY work with PHP!!!!
// If the mapped file exists, require it
if ($this->requireFile($baseDir, $file)) {
return $file;
}
}
// None of the directories contained the file
return false;
}
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;
}
}

Loading…
Cancel
Save