diff --git a/src/Bootstrap.php b/src/Bootstrap.php index f68c9a6..53393bd 100644 --- a/src/Bootstrap.php +++ b/src/Bootstrap.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(); diff --git a/src/Framework/Assets.php b/src/Framework/Assets.php index f9aedef..ffa0ce4 100644 --- a/src/Framework/Assets.php +++ b/src/Framework/Assets.php @@ -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 ''; - 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('&', "\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 ''; + } + } diff --git a/src/Framework/Common.php b/src/Framework/Common.php index 9f22faf..e33b6d3 100644 --- a/src/Framework/Common.php +++ b/src/Framework/Common.php @@ -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) { diff --git a/src/Framework/Enum/FieldFilter.php b/src/Framework/Enum/FieldFilter.php new file mode 100644 index 0000000..75b1cf6 --- /dev/null +++ b/src/Framework/Enum/FieldFilter.php @@ -0,0 +1,56 @@ + 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 + ], + }; + } +} diff --git a/src/Framework/Enum/Flags.php b/src/Framework/Enum/Flags.php new file mode 100644 index 0000000..04a3cd4 --- /dev/null +++ b/src/Framework/Enum/Flags.php @@ -0,0 +1,25 @@ +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, "
\n"); + $message = $this->getCodeNumber($e); + $message .= wordwrap($content, WORD_WRAP_CHRS, "
\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 "

" . $this->myErr . "

"; + return "

" . $this->getCodeNumber($e) . $this->myErr . "

"; } 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 "

" . $this->myErr . "

"; + echo "

" . $this->getCodeNumber($e) . $this->myErr . "

"; } private function setLoggerByLevel(Throwable $e): void diff --git a/src/Framework/HtmlDocument.php b/src/Framework/HtmlDocument.php index e5bf40f..c6028a6 100644 --- a/src/Framework/HtmlDocument.php +++ b/src/Framework/HtmlDocument.php @@ -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 = "" . PHP_EOL; diff --git a/src/Framework/Http/App/App.php b/src/Framework/Http/App/App.php index 17e5463..375da10 100644 --- a/src/Framework/Http/App/App.php +++ b/src/Framework/Http/App/App.php @@ -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); } /** diff --git a/src/Framework/Http/Kernel.php b/src/Framework/Http/Kernel.php index fe8b03a..d1c05ba 100644 --- a/src/Framework/Http/Kernel.php +++ b/src/Framework/Http/Kernel.php @@ -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) { diff --git a/src/Framework/Http/Request.php b/src/Framework/Http/Request.php index 208fe6b..b6eaddf 100644 --- a/src/Framework/Http/Request.php +++ b/src/Framework/Http/Request.php @@ -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] */ diff --git a/src/Framework/LoadAll.php b/src/Framework/LoadAll.php index 4c11f4b..200a3f6 100644 --- a/src/Framework/LoadAll.php +++ b/src/Framework/LoadAll.php @@ -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) { diff --git a/src/Framework/Middleware/ErrorMiddleware.php b/src/Framework/Middleware/ErrorMiddleware.php index 1eb267b..82217f5 100644 --- a/src/Framework/Middleware/ErrorMiddleware.php +++ b/src/Framework/Middleware/ErrorMiddleware.php @@ -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() ); diff --git a/src/Framework/ParagonCrypto/Crypto.php b/src/Framework/ParagonCrypto/Crypto.php index df8a651..811752e 100644 --- a/src/Framework/ParagonCrypto/Crypto.php +++ b/src/Framework/ParagonCrypto/Crypto.php @@ -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"; diff --git a/src/Framework/ParagonCrypto/PasswordStorage.php b/src/Framework/ParagonCrypto/PasswordStorage.php index 7fff020..5466c78 100644 --- a/src/Framework/ParagonCrypto/PasswordStorage.php +++ b/src/Framework/ParagonCrypto/PasswordStorage.php @@ -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( diff --git a/src/Framework/ParagonCrypto/SodiumStorage.php b/src/Framework/ParagonCrypto/SodiumStorage.php index 0d70fd1..f285068 100644 --- a/src/Framework/ParagonCrypto/SodiumStorage.php +++ b/src/Framework/ParagonCrypto/SodiumStorage.php @@ -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, diff --git a/src/Framework/Playground/HttpContainer.php b/src/Framework/Playground/HttpContainer.php deleted file mode 100644 index 4ccd560..0000000 --- a/src/Framework/Playground/HttpContainer.php +++ /dev/null @@ -1,109 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/src/Framework/Playground/JunkForNow.php b/src/Framework/Playground/JunkForNow.php deleted file mode 100644 index 184f037..0000000 --- a/src/Framework/Playground/JunkForNow.php +++ /dev/null @@ -1,79 +0,0 @@ - '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; - } -} diff --git a/src/Framework/Playground/RouteServiceProvider.php b/src/Framework/Playground/RouteServiceProvider.php deleted file mode 100644 index 1ffa46b..0000000 --- a/src/Framework/Playground/RouteServiceProvider.php +++ /dev/null @@ -1,77 +0,0 @@ -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']); - } - }); - } -} diff --git a/src/Framework/Playground/Router.php b/src/Framework/Playground/Router.php deleted file mode 100644 index c56faa5..0000000 --- a/src/Framework/Playground/Router.php +++ /dev/null @@ -1,492 +0,0 @@ - '(\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]; - } - -} \ No newline at end of file diff --git a/src/Framework/Playground/ServiceProvider.php b/src/Framework/Playground/ServiceProvider.php deleted file mode 100644 index 2bc0de5..0000000 --- a/src/Framework/Playground/ServiceProvider.php +++ /dev/null @@ -1,46 +0,0 @@ -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') - ); - }); - } -} - */ \ No newline at end of file diff --git a/src/Framework/Playground/pipeOp.php b/src/Framework/Playground/pipeOp.php deleted file mode 100644 index 485d6aa..0000000 --- a/src/Framework/Playground/pipeOp.php +++ /dev/null @@ -1,32 +0,0 @@ -) - * 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(...); - * - */ \ No newline at end of file diff --git a/src/Framework/SaferOutput.php b/src/Framework/SaferOutput.php new file mode 100644 index 0000000..ad039b4 --- /dev/null +++ b/src/Framework/SaferOutput.php @@ -0,0 +1,156 @@ + + 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); + } +} diff --git a/src/Framework/Security.php b/src/Framework/Security.php index 1b434b4..3f283a9 100644 --- a/src/Framework/Security.php +++ b/src/Framework/Security.php @@ -1,17 +1,19 @@ 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 = ['$','{','}','[','`',';']; + $meta = ['$', '{', '}', '[', ']', '`', ';']; + $escaped = ['$', '{', '}', '[', '`', ';']; // 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; - } + } } diff --git a/src/Framework/Services/Sessions/CookieSessionHandler.php b/src/Framework/Services/Sessions/CookieSessionHandler.php index 109e720..d67490d 100644 --- a/src/Framework/Services/Sessions/CookieSessionHandler.php +++ b/src/Framework/Services/Sessions/CookieSessionHandler.php @@ -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; } diff --git a/src/Framework/Trait/Security/CsrfTokenFunctions.php b/src/Framework/Trait/Security/CsrfTokenFunctions.php index ec2faa0..1e141df 100644 --- a/src/Framework/Trait/Security/CsrfTokenFunctions.php +++ b/src/Framework/Trait/Security/CsrfTokenFunctions.php @@ -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 ""; + return ""; } - /** - * 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; } } diff --git a/src/Framework/Trait/Security/SessionHijackingFunctions.php b/src/Framework/Trait/Security/SessionHijackingFunctions.php index 8e482c1..35f1625 100644 --- a/src/Framework/Trait/Security/SessionHijackingFunctions.php +++ b/src/Framework/Trait/Security/SessionHijackingFunctions.php @@ -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; + } + } + } diff --git a/src/Framework/View.php b/src/Framework/View.php index 9bf5831..85143fc 100644 --- a/src/Framework/View.php +++ b/src/Framework/View.php @@ -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) { diff --git a/src/Psr4AutoloaderClass.php b/src/Psr4AutoloaderClass.php index fbc2a30..b8a03a2 100644 --- a/src/Psr4AutoloaderClass.php +++ b/src/Psr4AutoloaderClass.php @@ -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; + } }