From fb733077eea161814c682b9c7a97e602aa93df8d Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Fri, 15 Feb 2019 19:30:11 +0100 Subject: [PATCH 1/9] Updated minor changes. Creates version 1.2.0-RC1 --- composer.json | 8 ++-- src/Config/config.security.php | 48 +------------------ src/FuzeWorks/Input.php | 80 ++++++++++++++++++++++++++----- src/FuzeWorks/Output.php | 4 +- src/FuzeWorks/Security.php | 3 +- src/FuzeWorks/URI.php | 2 - src/FuzeWorks/WebComponent.php | 85 ++++++++++++++++++++++++--------- src/FuzeWorks/WebController.php | 29 +---------- src/FuzeWorks/WebModel.php | 27 +---------- src/FuzeWorks/WebView.php | 8 +++- 10 files changed, 150 insertions(+), 144 deletions(-) diff --git a/composer.json b/composer.json index d630e82..954b4a0 100644 --- a/composer.json +++ b/composer.json @@ -14,12 +14,12 @@ ], "require": { "php": ">=7.1.0", - "fuzeworks/core": "dev-development", - "fuzeworks/mvcr": "dev-master" + "fuzeworks/mvcr": "1.2.0-RC2", + "fuzeworks/core": "1.2.0-RC2" }, + "minimum-stability": "RC", "require-dev": { - "phpunit/phpunit": "^7", - "fuzeworks/tracycomponent": "dev-master" + "phpunit/phpunit": "^7" }, "autoload": { "psr-4": { diff --git a/src/Config/config.security.php b/src/Config/config.security.php index 891cda4..47749b9 100644 --- a/src/Config/config.security.php +++ b/src/Config/config.security.php @@ -49,56 +49,10 @@ return [ | 'csrf_regenerate' = Regenerate token on every submission | 'csrf_exclude_uris' = Array of URIs which ignore CSRF checks */ - 'csrf_protection' => false, + 'csrf_protection' => true, 'csrf_token_name' => 'fw_csrf_token', 'csrf_cookie_name' => 'fw_csrf_cookie', 'csrf_expire' => 7200, 'csrf_regenerate' => TRUE, 'csrf_exclude_uris' => array(), - - /* - |-------------------------------------------------------------------------- - | Standardize newlines - |-------------------------------------------------------------------------- - | - | Determines whether to standardize newline characters in input data, - | meaning to replace \r\n, \r, \n occurrences with the PHP_EOL value. - | - | This is particularly useful for portability between UNIX-based OSes, - | (usually \n) and Windows (\r\n). - | - */ - 'standardize_newlines' => FALSE, - - /* - |-------------------------------------------------------------------------- - | Global XSS Filtering - |-------------------------------------------------------------------------- - | - | Determines whether the XSS filter is always active when GET, POST or - | COOKIE data is encountered - | - | WARNING: This feature is DEPRECATED and currently available only - | for backwards compatibility purposes! - | - */ - 'global_xss_filtering' => FALSE, - - /* - |-------------------------------------------------------------------------- - | Reverse Proxy IPs - |-------------------------------------------------------------------------- - | - | If your server is behind a reverse proxy, you must whitelist the proxy - | IP addresses from which CodeIgniter should trust headers such as - | HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify - | the visitor's IP address. - | - | You can use both an array or a comma-separated list of proxy addresses, - | as well as specifying whole subnets. Here are a few examples: - | - | Comma-separated: '10.0.1.200,192.168.5.0/24' - | Array: array('10.0.1.200', '192.168.5.0/24') - */ - 'proxy_ips' => '' ]; \ No newline at end of file diff --git a/src/FuzeWorks/Input.php b/src/FuzeWorks/Input.php index f354262..463eef8 100644 --- a/src/FuzeWorks/Input.php +++ b/src/FuzeWorks/Input.php @@ -40,6 +40,9 @@ namespace FuzeWorks; use FuzeWorks\ConfigORM\ConfigORM; use Tracy\Debugger; +/** + * @todo Implement remaining methods from OldInput + */ class Input { /** @@ -69,6 +72,10 @@ class Input // Set the configuration $this->webConfig = Factory::getInstance()->config->getConfig('web'); + // If not handling requests, do not continue + if (!WebComponent::$willHandleRequest) + return; + // Sanitize all global arrays $this->sanitizeGlobals(); @@ -116,6 +123,11 @@ class Input Debugger::errorHandler($severity, $message, $file, $line, $context); } + /** + * Restores global arrays before handling by processes outside of FuzeWorks + * + * @internal + */ public function restoreGlobalArrays() { Logger::logInfo('Restoring global $_GET, $_POST, $_SERVER, $_COOKIE arrays'); @@ -235,6 +247,8 @@ class Input } /** + * Used to fetch variables from the global arrays + * * @param string $arrayName * @param null $index * @param bool $xssClean @@ -266,55 +280,97 @@ class Input return ($xssClean === true ? $this->security->xss_clean($value) : $value); } + /** + * Fetch variables from the global $_GET array + * + * @param string|array|null $index + * @param bool $xssClean + * @return mixed + */ public function get($index = null, bool $xssClean = true) { return $this->getFromInputArray('get', $index, $xssClean); } + /** + * Fetch variables from the global $_POST array + * + * @param string|array|null $index + * @param bool $xssClean + * @return mixed + */ public function post($index = null, bool $xssClean = true) { return $this->getFromInputArray('post', $index, $xssClean); } + /** + * Fetch variables from the global $_POST or $_GET array. Tries POST first + * + * @param string|array|null $index + * @param bool $xssClean + * @return mixed + */ public function postGet($index, bool $xssClean = true) { return isset($this->inputArray['post'][$index]) ? $this->post($index, $xssClean) : $this->get($index, $xssClean); } + /** + * Fetch variables from the global $_GET or $_POST array. Tries GET first + * + * @param string|array|null $index + * @param bool $xssClean + * @return mixed + */ public function getPost($index, bool $xssClean = true) { return isset($this->inputArray['get'][$index]) ? $this->get($index, $xssClean) : $this->post($index, $xssClean); } + /** + * Fetch variables from the global $_COOKIE array + * + * @param string|array|null $index + * @param bool $xssClean + * @return mixed + */ public function cookie($index = null, bool $xssClean = true) { return $this->getFromInputArray('cookie', $index, $xssClean); } + /** + * Fetch variables from the global $_SERVER array + * + * @param string|array|null $index + * @param bool $xssClean + * @return mixed + */ public function server($index = null, bool $xssClean = true) { return $this->getFromInputArray('server', $index, $xssClean); } /** - * @todo Extend with OldInput functionality + * Fetch the HTTP_USER_AGENT variable from the $_SERVER array + * + * @param string|array|null $index + * @param bool $xssClean + * @return mixed */ - public function ip() - { - $ip = ''; - // Validate IP - - $valid = ( - (bool)filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) || - (bool)filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) - ); - } - public function userAgent(bool $xssClean = true): string { return $this->getFromInputArray('server', 'HTTP_USER_AGENT', $xssClean); } + /** + * Fetch the REQUEST_METHOD variable from the $_SERVER array + * + * @param string|array|null $index + * @param bool $xssClean + * @return mixed + */ public function method(bool $xssClean = true): string { return $this->getFromInputArray('server', 'REQUEST_METHOD', $xssClean); diff --git a/src/FuzeWorks/Output.php b/src/FuzeWorks/Output.php index 16797c9..cfd66e7 100644 --- a/src/FuzeWorks/Output.php +++ b/src/FuzeWorks/Output.php @@ -38,9 +38,11 @@ namespace FuzeWorks; use FuzeWorks\ConfigORM\ConfigORM; -use FuzeWorks\Event\HelperLoadEvent; use FuzeWorks\Exception\OutputException; +/** + * @todo Implement caching + */ class Output { diff --git a/src/FuzeWorks/Security.php b/src/FuzeWorks/Security.php index d42036c..8398f80 100644 --- a/src/FuzeWorks/Security.php +++ b/src/FuzeWorks/Security.php @@ -45,6 +45,7 @@ use FuzeWorks\Exception\{ConfigException, SecurityException, Exception}; * @author EllisLab Dev Team * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/) * @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/) + * @todo Complete rewrite */ class Security { @@ -200,7 +201,7 @@ class Security { $this->_csrf_set_hash(); } - $this->charset = strtoupper(Factory::getInstance()->config->get('web')->charset); + $this->charset = strtoupper(Factory::getInstance()->config->get('web')->get('charset')); } // -------------------------------------------------------------------- diff --git a/src/FuzeWorks/URI.php b/src/FuzeWorks/URI.php index 1763575..d135c5c 100644 --- a/src/FuzeWorks/URI.php +++ b/src/FuzeWorks/URI.php @@ -68,9 +68,7 @@ class URI $this->input = Factory::getInstance()->input; $this->config = Factory::getInstance()->config->getConfig('web'); if (WebComponent::$willHandleRequest) - { $this->determineUri(); - } } public function determineUri() diff --git a/src/FuzeWorks/WebComponent.php b/src/FuzeWorks/WebComponent.php index 451ec8e..a87a443 100644 --- a/src/FuzeWorks/WebComponent.php +++ b/src/FuzeWorks/WebComponent.php @@ -38,12 +38,14 @@ namespace FuzeWorks; use FuzeWorks\Exception\EventException; use FuzeWorks\Exception\Exception; +use FuzeWorks\Exception\HaltException; use FuzeWorks\Exception\NotFoundException; +use FuzeWorks\Exception\OutputException; +use FuzeWorks\Exception\RouterException; use FuzeWorks\Exception\WebException; class WebComponent implements iComponent { - /** * Whether WebComponent is configured to handle a web request * @@ -67,10 +69,6 @@ class WebComponent implements iComponent ]; } - /** - * @param Configurator $configurator - * @todo WebComponent will not always be running when added to FuzeWorks, move this into a separate method - */ public function onAddComponent(Configurator $configurator) { // Add dependencies @@ -95,23 +93,41 @@ class WebComponent implements iComponent { } + /** + * On initializing, Initialize UTF8 first, since it's a dependency for other componentClasses + */ public function init() { // First init UTF8 UTF8::init(); } + /** + * Enable the WebComponent to prepare for handling requests + */ public function enableComponent() { self::$willHandleRequest = true; } + /** + * Disable the WebComponent so it won't prepare for handling requests + */ public function disableComponent() { self::$willHandleRequest = false; } /** + * Handle a Web request. + * + * Retrieves URI string, routes this URI using the provided routes, + * appends output and adds listener to view output on shutdown. + * + * @return bool + * @throws HaltException + * @throws OutputException + * @throws RouterException * @throws WebException */ public function routeWebRequest(): bool @@ -119,31 +135,32 @@ class WebComponent implements iComponent if (!self::$willHandleRequest) throw new WebException("Could not route web request. WebComponent is not configured to handle requests"); - // Set the output to display when shutting down try { + // Set the output to display when shutting down Events::addListener(function () { /** @var Output $output */ Logger::logInfo("Parsing output..."); $output = Factory::getInstance()->output; $output->display(); }, 'coreShutdownEvent', Priority::NORMAL); + + // Create an error 500 page when a haltEvent is fired + Events::addListener([$this, 'haltEventListener'], 'haltExecutionEvent', Priority::NORMAL); } catch (EventException $e) { throw new WebException("Could not route web request. coreShutdownEvent threw EventException: '".$e->getMessage()."'"); } /** @var Router $router */ - $router = Factory::getInstance()->router; - - /** @var URI $uriObject */ - $uriObject = Factory::getInstance()->uri; - $uri = $uriObject->uriString(); - + /** @var URI $uri */ /** @var Output $output */ + $router = Factory::getInstance()->router; + $uri = Factory::getInstance()->uri; $output = Factory::getInstance()->output; // Attempt to load the requested page try { - $viewOutput = $router->route($uri); + $uriString = $uri->uriString(); + $viewOutput = $router->route($uriString); } catch (NotFoundException $e) { Logger::logWarning("Requested page not found. Requesting Error/error404 View"); $output->setStatusHeader(404); @@ -158,15 +175,6 @@ class WebComponent implements iComponent Logger::exceptionHandler($e, false); $viewOutput = 'ERROR 404. Page was not found.'; } - } catch (Exception $e) { - Logger::exceptionHandler($e, false); - $output->setStatusHeader(500); - try { - $viewOutput = $router->route('Error/error500'); - } catch (Exception $error500Exception) { - Logger::exceptionHandler($error500Exception, false); - $viewOutput = 'ERROR 500. Page could not be loaded.'; - } } // Append the output @@ -175,4 +183,37 @@ class WebComponent implements iComponent return true; } + + /** + * Listener for haltExecutionEvent + * + * Fired when FuzeWorks halts it's execution. Loads an error 500 page. + * + * @param $event + */ + public function haltEventListener($event) + { + // Dependencies + /** @var Output $output */ + /** @var Router $router */ + /** @var Event $event */ + $output = Factory::getInstance()->output; + $router = Factory::getInstance()->router; + + // Cancel event + $event->setCancelled(true); + + try { + // And handle consequences + Logger::logError("Execution halted. Providing error 500 page."); + $output->setStatusHeader(500); + $viewOutput = $router->route('Error/error500'); + } catch (Exception $error500Exception) { + Logger::exceptionHandler($error500Exception, false); + $viewOutput = 'ERROR 500. Page could not be loaded.'; + } + + // Finally append output and shutdown + $output->appendOutput($viewOutput); + } } \ No newline at end of file diff --git a/src/FuzeWorks/WebController.php b/src/FuzeWorks/WebController.php index bc0dbec..00734b7 100644 --- a/src/FuzeWorks/WebController.php +++ b/src/FuzeWorks/WebController.php @@ -36,33 +36,6 @@ namespace FuzeWorks; -class WebController extends Controller +abstract class WebController extends Controller { - - /** - * @var Input - */ - protected $input; - - /** - * @var Output - */ - protected $output; - - /** - * @var URI - */ - protected $uri; - - /** - * WebView constructor. - */ - public function __construct() - { - parent::__construct(); - $this->input = Factory::getInstance()->input; - $this->output = Factory::getInstance()->output; - $this->uri = Factory::getInstance()->uri; - } - } \ No newline at end of file diff --git a/src/FuzeWorks/WebModel.php b/src/FuzeWorks/WebModel.php index 92207e5..75c20a2 100644 --- a/src/FuzeWorks/WebModel.php +++ b/src/FuzeWorks/WebModel.php @@ -36,31 +36,6 @@ namespace FuzeWorks; -class WebModel extends Model +abstract class WebModel extends Model { - /** - * @var Input - */ - protected $input; - - /** - * @var Output - */ - protected $output; - - /** - * @var URI - */ - protected $uri; - - /** - * WebView constructor. - */ - public function __construct() - { - parent::__construct(); - $this->input = Factory::getInstance()->input; - $this->output = Factory::getInstance()->output; - $this->uri = Factory::getInstance()->uri; - } } \ No newline at end of file diff --git a/src/FuzeWorks/WebView.php b/src/FuzeWorks/WebView.php index 02fc2fd..6e4bd78 100644 --- a/src/FuzeWorks/WebView.php +++ b/src/FuzeWorks/WebView.php @@ -36,7 +36,7 @@ namespace FuzeWorks; -class WebView extends View +abstract class WebView extends View { /** * @var Input @@ -53,6 +53,11 @@ class WebView extends View */ protected $uri; + /** + * @var Security + */ + protected $security; + /** * WebView constructor. */ @@ -62,5 +67,6 @@ class WebView extends View $this->input = Factory::getInstance()->input; $this->output = Factory::getInstance()->output; $this->uri = Factory::getInstance()->uri; + $this->security = Factory::getInstance()->security; } } \ No newline at end of file From 416d3895d0eddae132879a5d3aa918c01c750350 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Fri, 1 Mar 2019 11:25:18 +0100 Subject: [PATCH 2/9] Implemented changes requested by FuzeWorks\Application - CSRF Verification is now functional again - CSRF throws CSRFException - XSS clean can now be disabled in the config globally. Once disabled, this can't be overridden - X-Powered-By header is now always suppressed - WebComponent now assigns global variables to the LayoutEngine. - CSRF Exceptions can be handled by a view, if this one implements the securityExceptionHandler() method - Error403 page added --- src/Config/config.security.php | 10 +- src/FuzeWorks/Exception/CSRFException.php | 42 ++++++++ src/FuzeWorks/Input.php | 27 ++++- src/FuzeWorks/Output.php | 3 + src/FuzeWorks/Security.php | 121 ++++++++-------------- src/FuzeWorks/URI.php | 29 +----- src/FuzeWorks/WebComponent.php | 97 ++++++++++++++++- 7 files changed, 215 insertions(+), 114 deletions(-) create mode 100644 src/FuzeWorks/Exception/CSRFException.php diff --git a/src/Config/config.security.php b/src/Config/config.security.php index 47749b9..346fcbc 100644 --- a/src/Config/config.security.php +++ b/src/Config/config.security.php @@ -51,8 +51,14 @@ return [ */ 'csrf_protection' => true, 'csrf_token_name' => 'fw_csrf_token', - 'csrf_cookie_name' => 'fw_csrf_cookie', 'csrf_expire' => 7200, - 'csrf_regenerate' => TRUE, 'csrf_exclude_uris' => array(), + + // CSRF Cookie information + 'csrf_cookie_name' => 'fw_csrf_cookie', + 'csrf_cookie_prefix' => '', + 'csrf_cookie_domain' => '', + 'csrf_cookie_path' => '/', + 'csrf_cookie_secure' => false, + 'csrf_cookie_httponly' => false ]; \ No newline at end of file diff --git a/src/FuzeWorks/Exception/CSRFException.php b/src/FuzeWorks/Exception/CSRFException.php new file mode 100644 index 0000000..9ffbb13 --- /dev/null +++ b/src/FuzeWorks/Exception/CSRFException.php @@ -0,0 +1,42 @@ +webConfig->get('xss_clean'); + // Never run XSS clean if disabled by config + $xssClean = ($this->webConfig->get('xss_clean') == true ? $xssClean : false); // If the index is null, the entire array is requested $index = (!is_null($index) ? $index : array_keys($this->inputArray[$arrayName])); @@ -376,5 +376,28 @@ class Input return $this->getFromInputArray('server', 'REQUEST_METHOD', $xssClean); } + /** + * Is HTTPS? + * + * Determines if the application is accessed via an encrypted + * (HTTPS) connection. + * + * @return bool + */ + public function isHttps(): bool + { + if (!empty($this->inputArray['server']['HTTPS']) && strtolower($this->inputArray['server']['HTTPS']) !== 'off') + return true; + + elseif (isset($this->inputArray['server']['HTTP_X_FORWARDED_PROTO']) && $this->inputArray['server']['HTTP_X_FORWARDED_PROTO'] === 'https') + return true; + + elseif ( ! empty($this->inputArray['server']['HTTP_FRONT_END_HTTPS']) && strtolower($this->inputArray['server']['HTTP_FRONT_END_HTTPS']) !== 'off') + return true; + + return false; + } + + } \ No newline at end of file diff --git a/src/FuzeWorks/Output.php b/src/FuzeWorks/Output.php index cfd66e7..b77197d 100644 --- a/src/FuzeWorks/Output.php +++ b/src/FuzeWorks/Output.php @@ -163,6 +163,9 @@ class Output ob_start('ob_gzhandler'); } + // Remove the X-Powered-By header, since it's a security risk + header_remove("X-Powered-By"); + // Send all available headers if (!empty($this->headers)) foreach ($this->headers as $header) diff --git a/src/FuzeWorks/Security.php b/src/FuzeWorks/Security.php index 8398f80..99be132 100644 --- a/src/FuzeWorks/Security.php +++ b/src/FuzeWorks/Security.php @@ -37,7 +37,7 @@ namespace FuzeWorks; use FuzeWorks\ConfigORM\ConfigORM; -use FuzeWorks\Exception\{ConfigException, SecurityException, Exception}; +use FuzeWorks\Exception\{ConfigException, CSRFException, Exception}; /** * Security Class @@ -169,6 +169,13 @@ class Security { */ private $config; + /** + * Input. A dependency for this class + * + * @var Input + */ + private $input; + /** * Class constructor * @@ -178,6 +185,7 @@ class Security { public function init() { $this->config = Factory::getInstance()->config->get('security'); + $this->input = Factory::getInstance()->input; // Is CSRF protection enabled? if ($this->config->csrf_protection) @@ -210,14 +218,13 @@ class Security { * CSRF Verify * * @return self + * @throws CSRFException */ public function csrf_verify(): self { // If it's not a POST request we will set the CSRF cookie - if (strtoupper($_SERVER['REQUEST_METHOD']) !== 'POST') - { + if (strtoupper($this->input->server('REQUEST_METHOD')) !== 'POST') return $this->csrf_set_cookie(); - } // Check if URI has been whitelisted from CSRF checks if ($exclude_uris = $this->config->csrf_exclude_uris) @@ -232,22 +239,10 @@ class Security { } // Do the tokens exist in both the _POST and _COOKIE arrays? - if ( ! isset($_POST[$this->_csrf_token_name], $_COOKIE[$this->_csrf_cookie_name]) - OR $_POST[$this->_csrf_token_name] !== $_COOKIE[$this->_csrf_cookie_name]) // Do the tokens match? - { + $token = $this->input->post($this->_csrf_token_name); + $cookie = $this->input->cookie($this->_csrf_cookie_name); + if ($token == null || $cookie == null || $token !== $cookie) $this->csrf_show_error(); - } - - // We kill this since we're done and we don't want to polute the _POST array - unset($_POST[$this->_csrf_token_name]); - - // Regenerate on every submission? - if ($this->config->csrf_regenerate) - { - // Nothing should last forever - unset($_COOKIE[$this->_csrf_cookie_name]); - $this->_csrf_hash = NULL; - } $this->_csrf_set_hash(); $this->csrf_set_cookie(); @@ -258,6 +253,22 @@ class Security { // -------------------------------------------------------------------- + /** + * CSRF Regenerate + * + * @throws ConfigException + * @return self + */ + public function csrf_regenerate() + { + Logger::log("CSRF Hash is being regenerated..."); + $this->_csrf_hash = null; + $this->_csrf_set_hash(); + $this->csrf_set_cookie(); + + return $this; + } + /** * CSRF Set Cookie * @@ -267,24 +278,25 @@ class Security { public function csrf_set_cookie() { $expire = time() + $this->_csrf_expire; - $cfg = Factory::getInstance()->config->get('main'); - $secure_cookie = (bool) $cfg->cookie_secure; + $cfg = Factory::getInstance()->config->get('security'); + $secure_cookie = (bool) $cfg->csrf_cookie_secure; - if ($secure_cookie && ! Core::isHttps()) - { + if ($secure_cookie && !$this->input->isHttps()) return $this; - } setcookie( $this->_csrf_cookie_name, $this->_csrf_hash, $expire, - $cfg->cookie_path, - $cfg->cookie_domain, + $cfg->csrf_cookie_path, + $cfg->csrf_cookie_domain, $secure_cookie, - $cfg->cookie_httponly + $cfg->csrf_cookie_httponly ); + Logger::log('CSRF cookie sent'); + + return $this; } // -------------------------------------------------------------------- @@ -293,11 +305,11 @@ class Security { * Show CSRF Error * * @return void - * @throws SecurityException + * @throws CSRFException */ public function csrf_show_error() { - throw new SecurityException('The action you have requested is not allowed.', 1); + throw new CSRFException('This action resulted in a Cross Site Reference Forgery warning. Request will be blocked...', 5); } // -------------------------------------------------------------------- @@ -728,57 +740,6 @@ class Security { return $str; } - // -------------------------------------------------------------------- - - /** - * Sanitize Filename - * - * @param string $str Input file name - * @param bool $relative_path Whether to preserve paths - * @return string - */ - public function sanitize_filename($str, $relative_path = FALSE): string - { - $bad = $this->filename_bad_chars; - - if ( ! $relative_path) - { - $bad[] = './'; - $bad[] = '/'; - } - - $str = UTF8::removeInvisibleCharacters($str, FALSE); - - do - { - $old = $str; - $str = str_replace($bad, '', $str); - } - while ($old !== $str); - - return stripslashes($str); - } - - // ---------------------------------------------------------------- - - /** - * Strip Image Tags - * - * @param string $str - * @return string - */ - public function strip_image_tags($str): string - { - return preg_replace( - array( - '##i', - '#`]+)).*?\>#i' - ), - '\\2', - $str - ); - } - // ---------------------------------------------------------------- /** diff --git a/src/FuzeWorks/URI.php b/src/FuzeWorks/URI.php index d135c5c..29fa54b 100644 --- a/src/FuzeWorks/URI.php +++ b/src/FuzeWorks/URI.php @@ -84,7 +84,7 @@ class URI $scriptName = $this->input->server('SCRIPT_NAME'); $scriptFilename = $this->input->server('SCRIPT_FILENAME'); - $baseUrl = ($this->isHttps() ? 'https' : 'http') . + $baseUrl = ($this->input->isHttps() ? 'https' : 'http') . "://" . $serverAddr . substr($scriptName, 0, strpos($scriptName, basename($scriptFilename))); } @@ -222,31 +222,4 @@ class URI return true; } - /** - * Is HTTPS? - * - * Determines if the application is accessed via an encrypted - * (HTTPS) connection. - * - * @return bool - */ - protected function isHttps(): bool - { - $https = $this->input->server('HTTPS'); - if (!is_null($https) && strtolower($https) !== 'off') - { - return true; - } - elseif (!is_null($this->input->server('HTTP_X_FORWARDED_PROTO')) && $this->input->server('HTTP_X_FORWARDED_PROTO') === 'https') - { - return true; - } - elseif ( ! is_null($this->input->server('HTTP_FRONT_END_HTTPS')) && strtolower($this->input->server('HTTP_FRONT_END_HTTPS')) !== 'off') - { - return true; - } - - return false; - } - } \ No newline at end of file diff --git a/src/FuzeWorks/WebComponent.php b/src/FuzeWorks/WebComponent.php index a87a443..f40723d 100644 --- a/src/FuzeWorks/WebComponent.php +++ b/src/FuzeWorks/WebComponent.php @@ -36,12 +36,17 @@ namespace FuzeWorks; +use FuzeWorks\Event\HaltExecutionEvent; +use FuzeWorks\Event\LayoutLoadEvent; +use FuzeWorks\Event\RouterCallViewEvent; +use FuzeWorks\Exception\CSRFException; use FuzeWorks\Exception\EventException; use FuzeWorks\Exception\Exception; use FuzeWorks\Exception\HaltException; use FuzeWorks\Exception\NotFoundException; use FuzeWorks\Exception\OutputException; use FuzeWorks\Exception\RouterException; +use FuzeWorks\Exception\SecurityException; use FuzeWorks\Exception\WebException; class WebComponent implements iComponent @@ -100,6 +105,9 @@ class WebComponent implements iComponent { // First init UTF8 UTF8::init(); + + // Register some base events + Events::addListener([$this, 'layoutLoadEventListener'], 'layoutLoadEvent', Priority::NORMAL); } /** @@ -137,7 +145,7 @@ class WebComponent implements iComponent try { // Set the output to display when shutting down - Events::addListener(function () { + Events::addListener(function ($event) { /** @var Output $output */ Logger::logInfo("Parsing output..."); $output = Factory::getInstance()->output; @@ -153,9 +161,25 @@ class WebComponent implements iComponent /** @var Router $router */ /** @var URI $uri */ /** @var Output $output */ + /** @var Security $security */ $router = Factory::getInstance()->router; $uri = Factory::getInstance()->uri; $output = Factory::getInstance()->output; + $security = Factory::getInstance()->security; + + // And start logging the request + Logger::newLevel("Routing web request..."); + + // First test for Cross Site Request Forgery + try { + $security->csrf_verify(); + } catch (SecurityException $exception) { + // If a SecurityException is thrown, first log it + Logger::logWarning("SecurityException thrown. Registering listener to verify handler in View"); + + // Register a listener + Events::addListener([$this, 'callViewEventListener'], 'routerCallViewEvent', Priority::HIGHEST, $exception); + } // Attempt to load the requested page try { @@ -165,6 +189,9 @@ class WebComponent implements iComponent Logger::logWarning("Requested page not found. Requesting Error/error404 View"); $output->setStatusHeader(404); + // Remove listener so that error pages won't be intercepted + Events::removeListener([$this, 'callViewEventListener'], 'routerCallViewEvent',Priority::HIGHEST); + // Request 404 page= try { $viewOutput = $router->route('Error/error404'); @@ -175,15 +202,54 @@ class WebComponent implements iComponent Logger::exceptionHandler($e, false); $viewOutput = 'ERROR 404. Page was not found.'; } + } catch (HaltException $e) { + Logger::logWarning("Requested page was denied. Requesting Error/error403 View."); + $output->setStatusHeader(403); + + // Remove listener so that error pages won't be intercepted + Events::removeListener([$this, 'callViewEventListener'], 'routerCallViewEvent',Priority::HIGHEST); + + try { + $viewOutput = $router->route('Error/error403'); + } catch (NotFoundException $e) { + // If still resulting in an error, do something else + $viewOutput = 'ERROR 403. Forbidden.'; + } catch (Exception $e) { + Logger::exceptionHandler($e, false); + $viewOutput = 'ERROR 403. Forbidden.'; + } } // Append the output if (!empty($viewOutput)) $output->appendOutput($viewOutput); + Logger::stopLevel(); return true; } + /** + * Listener for routerCallViewEvent + * + * Fired when a SecurityException is thrown. Verifies if a securityExceptionHandler() method exists. + * If not, the calling of the view is cancelled. If yes, the calling of the view depends on the + * result of the method + * + * @param RouterCallViewEvent $event + * @param SecurityException $exception + */ + public function callViewEventListener(RouterCallViewEvent $event, SecurityException $exception) + { + /** @var RouterCallViewEvent $event */ + // If the securityExceptionHandler method exists, cancel based on that methods output + if (method_exists($event->view, 'securityExceptionHandler')) + $event->setCancelled(!$event->view->securityExceptionHandler($exception)); + + // If not, cancel it immediately + else + $event->setCancelled(true); + } + /** * Listener for haltExecutionEvent * @@ -191,7 +257,7 @@ class WebComponent implements iComponent * * @param $event */ - public function haltEventListener($event) + public function haltEventListener(HaltExecutionEvent $event) { // Dependencies /** @var Output $output */ @@ -203,6 +269,9 @@ class WebComponent implements iComponent // Cancel event $event->setCancelled(true); + // Remove listener so that error pages won't be intercepted + Events::removeListener([$this, 'callViewEventListener'], 'routerCallViewEvent',Priority::HIGHEST); + try { // And handle consequences Logger::logError("Execution halted. Providing error 500 page."); @@ -216,4 +285,28 @@ class WebComponent implements iComponent // Finally append output and shutdown $output->appendOutput($viewOutput); } + + /** + * Listener for layoutLoadEvent + * + * Assigns variables from the WebComponent to Layout engines. + * + * @param $event + * @throws Exception\ConfigException + */ + public function layoutLoadEventListener(LayoutLoadEvent $event) + { + // Dependencies + /** @var Security $security */ + /** @var Config $config */ + $security = Factory::getInstance()->security; + $config = Factory::getInstance()->config; + + /** @var LayoutLoadEvent $event */ + $event->assign('csrfHash', $security->get_csrf_hash()); + $event->assign('csrfTokenName', $security->get_csrf_token_name()); + $event->assign('siteURL', $config->getConfig('web')->get('base_url')); + + Logger::logInfo("Assigned variables to TemplateEngine from WebComponent"); + } } \ No newline at end of file From 208d2783c70be1fa341cbea6278e91c8a705b211 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Tue, 5 Mar 2019 11:23:52 +0100 Subject: [PATCH 3/9] Release 1.2.0-RC3 --- composer.json | 5 +- src/FuzeWorks/Input.php | 37 +---- src/FuzeWorks/Output.php | 275 +++++++++++++++++++++++++++------ src/FuzeWorks/WebComponent.php | 25 ++- 4 files changed, 258 insertions(+), 84 deletions(-) diff --git a/composer.json b/composer.json index 954b4a0..012f6d5 100644 --- a/composer.json +++ b/composer.json @@ -14,10 +14,11 @@ ], "require": { "php": ">=7.1.0", - "fuzeworks/mvcr": "1.2.0-RC2", - "fuzeworks/core": "1.2.0-RC2" + "fuzeworks/mvcr": "1.2.0-RC3", + "fuzeworks/core": "1.2.0-RC3" }, "minimum-stability": "RC", + "prefer-stable": true, "require-dev": { "phpunit/phpunit": "^7" }, diff --git a/src/FuzeWorks/Input.php b/src/FuzeWorks/Input.php index 795737a..0e3ad12 100644 --- a/src/FuzeWorks/Input.php +++ b/src/FuzeWorks/Input.php @@ -1,4 +1,4 @@ -restoreGlobalArrays(); - Debugger::exceptionHandler($exception, $exit); - } - - /** - * Used to restore global arrays before handling errors by Tracy - * - * @param $severity - * @param $message - * @param $file - * @param $line - * @param array $context - * @throws \ErrorException - * @internal - */ - public function tracyErrorHandler($severity, $message, $file, $line, $context = []) - { - $this->restoreGlobalArrays(); - Debugger::errorHandler($severity, $message, $file, $line, $context); - } - /** * Restores global arrays before handling by processes outside of FuzeWorks * diff --git a/src/FuzeWorks/Output.php b/src/FuzeWorks/Output.php index b77197d..a623f14 100644 --- a/src/FuzeWorks/Output.php +++ b/src/FuzeWorks/Output.php @@ -53,6 +53,13 @@ class Output */ private $input; + /** + * The internal URI class + * + * @var URI + */ + private $uri; + /** * WebCfg * @@ -84,12 +91,38 @@ class Output public $mimes = []; protected $mimeType = 'text/html'; + /** + * The amount of time the current page is cached + * + * @var int $cacheTime + */ + protected $cacheTime = 0; + + /** + * Whether a cache file is being used now + * + * @var bool + */ + protected $usingCache = false; + + /** + * The status code that will be sent to the client + * + * @var int $statusCode + */ protected $statusCode = 200; + + /** + * The status code text that will be sent along with $statusCode + * + * @var string $statusText + */ protected $statusText = 'OK'; public function init() { $this->input = Factory::getInstance()->input; + $this->uri = Factory::getInstance()->uri; $this->mimes = Factory::getInstance()->config->getConfig('mimes')->toArray(); $this->config = Factory::getInstance()->config->getConfig('web'); @@ -97,6 +130,200 @@ class Output $this->compressOutput = (!$zlib && $this->config->get('compress_output') && extension_loaded('zlib')); } + /** + * Display Output + * + * Processes and sends finalized output data to the browser along + * with any server headers. + * + * @param string $output Output data override + * @return void + */ + public function display(string $output = null) + { + // Set the output data + $output = is_null($output) ? $this->output : $output; + + // Write cache if requested to do so + if ($this->cacheTime > 0) + $this->writeCache($output); + + // First send status code + http_response_code($this->statusCode); + @header('Status: ' . $this->statusCode . ' ' . $this->statusText, true); + + // If compression is requested, start buffering + if ( + $this->compressOutput && !$this->usingCache && + !is_null($this->input->server('HTTP_ACCEPT_ENCODING')) && + strpos($this->input->server('HTTP_ACCEPT_ENCODING'), 'gzip') !== false + ) + { + Logger::log("Compressing output..."); + ob_start('ob_gzhandler'); + } + + // Send gzip headers when using cache + if ($this->usingCache && $this->compressOutput) + { + if (!is_null($this->input->server('HTTP_ACCEPT_ENCODING')) && + strpos($this->input->server('HTTP_ACCEPT_ENCODING'), 'gzip') !== false) + { + header('Content-Encoding: gzip'); + header('Content-Length: '.strlen($output)); + } + // If the cache is zipped, but the client doesn't support it, decompress the output + else + $output = gzinflate(substr($output, 10, -8)); + } + + // Send all available headers + if (!empty($this->headers)) + foreach ($this->headers as $header) + @header($header[0], $header[1]); + + echo $output; + + Logger::log('Output sent to browser'); + } + + /** + * Enable the current page to be cached + * + * Set the amount of time with the $time parameter. + * + * @param int $time In minutes + */ + public function cache(int $time) + { + $this->cacheTime = $time > 0 ? $time : 0; + } + + public function getCache(string $selector): bool + { + // If empty, index page is requested + $selector = empty($selector) ? 'index' : $selector; + + // Generate the full uri + $uri = $this->config->get('base_url') . $selector; + + // Determine the file that holds the cache + if ($this->compressOutput) + $file = Core::$tempDir . DS . 'OutputCache' . DS . md5($uri) . '_gzip.fwcache'; + else + $file = Core::$tempDir . DS . 'OutputCache' . DS . md5($uri) . '.fwcache'; + + + // Determine if file exists + if (!file_exists($file)) + return false; + + // Retrieve cache + $cache = file_get_contents($file); + + // Verify that this is a cache file + if (!preg_match('/^(.*)EndFuzeWorksCache--->/', $cache, $match)) + return false; + + // Retrieve data from cache file + $cacheInfo = unserialize($match[1]); + + // Test if the cache has expired + if (time() > $cacheInfo['expire']) + { + // If not writeable, log warning and do not remove + if (!Core::isReallyWritable($file)) + { + Logger::logWarning("Found expired output cache. Could not remove!"); + return false; + } + + // Delete file if expired + @unlink($file); + Logger::logInfo("Found expired output cache. Removed."); + return false; + } + + // @todo Send cache header + + // Send all the headers cached in the file + foreach ($cacheInfo['headers'] as $header) + $this->setHeader($header[0], $header[1]); + + // And save the output + $this->usingCache = true; + $this->setOutput(substr($cache, strlen($match[0]))); + Logger::logInfo("Found output cache. Set output."); + return true; + } + + public function writeCache(string $output) + { + // First create cache directory + $cachePath = Core::$tempDir . DS . 'OutputCache'; + + // Attempt to create the OutputCache directory in the TempDirectory + if (!is_dir($cachePath) && !mkdir($cachePath, 0777, false)) + { + Logger::logError("Could not write output cache. Cannot create directory. Are permissions set correctly?"); + return false; + } + + // If directory is not writable, return error + if (!Core::isReallyWritable($cachePath)) + { + Logger::logError("Could not write output cache. No file permissions. Are permissions set correctly?"); + return false; + } + + // Generate the full uri + $uri = $this->config->get('base_url') . (empty($this->uri->uriString()) ? 'index' : $this->uri->uriString()); + + // Determine the file that holds the cache + if ($this->compressOutput) + $file = $cachePath . DS . md5($uri) . '_gzip.fwcache'; + else + $file = $cachePath . DS . md5($uri) . '.fwcache'; + + + // If compression is enabled, compress the output + if ($this->compressOutput) + { + $output = gzencode($output); + if ($this->getHeader('content-type') === null) + $this->setContentType($this->mimeType); + } + + // Calculate expiry time + $expire = time() + ($this->cacheTime * 60); + + // Prepare the cache contents + $cache = [ + 'expire' => $expire, + 'headers' => $this->headers + ]; + + // Create cache file contents + $cache = serialize($cache) . 'EndFuzeWorksCache--->' . $output; + + // Write the cache + if (file_put_contents($file, $cache, LOCK_EX) === false) + { + @unlink($file); + Logger::logError("Could not write output cache. File error. Deleting cache file."); + return false; + } + + // Lowering permissions to read only + chmod($cachePath, 0640); + + // And report back + Logger::logInfo("Output cache has been saved."); + + // @todo Set cache header + return true; + } + /** * Get Output * @@ -134,48 +361,6 @@ class Output $this->output .= $output; } - /** - * Display Output - * - * Processes and sends finalized output data to the browser along - * with any server headers. - * - * @param string $output Output data override - * @return void - */ - public function display(string $output = null) - { - // Set the output data - $output = is_null($output) ? $this->output : $output; - - // First send status code - http_response_code($this->statusCode); - @header('Status: ' . $this->statusCode . ' ' . $this->statusText, true); - - // If compression is requested, start buffering - if ( - $this->compressOutput && - !is_null($this->input->server('HTTP_ACCEPT_ENCODING')) && - strpos($this->input->server('HTTP_ACCEPT_ENCODING'), 'gzip') !== false - ) - { - Logger::log("Compressing output..."); - ob_start('ob_gzhandler'); - } - - // Remove the X-Powered-By header, since it's a security risk - header_remove("X-Powered-By"); - - // Send all available headers - if (!empty($this->headers)) - foreach ($this->headers as $header) - @header($header[0], $header[1]); - - echo $output; - - Logger::log('Output sent to browser'); - } - /** * Set Header * @@ -186,6 +371,11 @@ class Output */ public function setHeader(string $header, bool $replace = true) { + // If compression is enabled content-length should be suppressed, since it won't match the length + // of the compressed output. + if ($this->compressOutput && strncasecmp($header, 'content-length', 14) === 0) + return; + $this->headers[] = [$header, $replace]; } @@ -219,7 +409,6 @@ class Output * @param string $mimeType Extension of the file we're outputting * @param string $charset Character set (default: NULL) */ - public function setContentType(string $mimeType, $charset = null) { if (strpos($mimeType, '/') === false) diff --git a/src/FuzeWorks/WebComponent.php b/src/FuzeWorks/WebComponent.php index f40723d..13714e8 100644 --- a/src/FuzeWorks/WebComponent.php +++ b/src/FuzeWorks/WebComponent.php @@ -39,6 +39,7 @@ namespace FuzeWorks; use FuzeWorks\Event\HaltExecutionEvent; use FuzeWorks\Event\LayoutLoadEvent; use FuzeWorks\Event\RouterCallViewEvent; +use FuzeWorks\Exception\ConfigException; use FuzeWorks\Exception\CSRFException; use FuzeWorks\Exception\EventException; use FuzeWorks\Exception\Exception; @@ -133,10 +134,10 @@ class WebComponent implements iComponent * appends output and adds listener to view output on shutdown. * * @return bool - * @throws HaltException * @throws OutputException * @throws RouterException * @throws WebException + * @throws EventException */ public function routeWebRequest(): bool { @@ -158,6 +159,9 @@ class WebComponent implements iComponent throw new WebException("Could not route web request. coreShutdownEvent threw EventException: '".$e->getMessage()."'"); } + // Remove the X-Powered-By header, since it's a security risk + header_remove("X-Powered-By"); + /** @var Router $router */ /** @var URI $uri */ /** @var Output $output */ @@ -170,6 +174,11 @@ class WebComponent implements iComponent // And start logging the request Logger::newLevel("Routing web request..."); + // First check if a cached page is available + $uriString = $uri->uriString(); + if ($output->getCache($uriString)) + return true; + // First test for Cross Site Request Forgery try { $security->csrf_verify(); @@ -183,7 +192,6 @@ class WebComponent implements iComponent // Attempt to load the requested page try { - $uriString = $uri->uriString(); $viewOutput = $router->route($uriString); } catch (NotFoundException $e) { Logger::logWarning("Requested page not found. Requesting Error/error404 View"); @@ -256,6 +264,8 @@ class WebComponent implements iComponent * Fired when FuzeWorks halts it's execution. Loads an error 500 page. * * @param $event + * @throws EventException + * @TODO remove FuzeWorks\Layout dependency */ public function haltEventListener(HaltExecutionEvent $event) { @@ -263,12 +273,17 @@ class WebComponent implements iComponent /** @var Output $output */ /** @var Router $router */ /** @var Event $event */ + /** @var Layout $layout */ $output = Factory::getInstance()->output; $router = Factory::getInstance()->router; + $layout = Factory::getInstance()->layouts; // Cancel event $event->setCancelled(true); + // Reset the layout engine + $layout->reset(); + // Remove listener so that error pages won't be intercepted Events::removeListener([$this, 'callViewEventListener'], 'routerCallViewEvent',Priority::HIGHEST); @@ -291,10 +306,10 @@ class WebComponent implements iComponent * * Assigns variables from the WebComponent to Layout engines. * - * @param $event - * @throws Exception\ConfigException + * @param LayoutLoadEvent $event + * @throws ConfigException */ - public function layoutLoadEventListener(LayoutLoadEvent $event) + public function layoutLoadEventListener($event) { // Dependencies /** @var Security $security */ From 612ab3abca448029c9666fca92d0026b597f47bb Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Mon, 22 Jul 2019 11:48:45 +0200 Subject: [PATCH 4/9] Implemented the RouteWebRequestEvent and sessions. - Developers can now use the RouteWebRequestEvent to cancel the loading of web requests - FuzeWorks\Input now starts the PHP session, which is required for some dependencies --- src/FuzeWorks/Event/RouteWebRequestEvent.php | 53 ++++++++++++++++++++ src/FuzeWorks/Input.php | 3 ++ src/FuzeWorks/WebComponent.php | 7 +++ 3 files changed, 63 insertions(+) create mode 100644 src/FuzeWorks/Event/RouteWebRequestEvent.php diff --git a/src/FuzeWorks/Event/RouteWebRequestEvent.php b/src/FuzeWorks/Event/RouteWebRequestEvent.php new file mode 100644 index 0000000..58ac9f1 --- /dev/null +++ b/src/FuzeWorks/Event/RouteWebRequestEvent.php @@ -0,0 +1,53 @@ +uriString = $uriString; + } + +} \ No newline at end of file diff --git a/src/FuzeWorks/Input.php b/src/FuzeWorks/Input.php index 0e3ad12..6519e48 100644 --- a/src/FuzeWorks/Input.php +++ b/src/FuzeWorks/Input.php @@ -75,6 +75,9 @@ class Input if (!WebComponent::$willHandleRequest) return; + // Start session + session_start(); + // Sanitize all global arrays $this->sanitizeGlobals(); diff --git a/src/FuzeWorks/WebComponent.php b/src/FuzeWorks/WebComponent.php index 13714e8..2a7e762 100644 --- a/src/FuzeWorks/WebComponent.php +++ b/src/FuzeWorks/WebComponent.php @@ -151,6 +151,7 @@ class WebComponent implements iComponent Logger::logInfo("Parsing output..."); $output = Factory::getInstance()->output; $output->display(); + return $event; }, 'coreShutdownEvent', Priority::NORMAL); // Create an error 500 page when a haltEvent is fired @@ -179,6 +180,11 @@ class WebComponent implements iComponent if ($output->getCache($uriString)) return true; + // Send webRequestEvent, if no cache is found + $event = Events::fireEvent('routeWebRequestEvent', $uriString); + if ($event->isCancelled()) + return true; + // First test for Cross Site Request Forgery try { $security->csrf_verify(); @@ -321,6 +327,7 @@ class WebComponent implements iComponent $event->assign('csrfHash', $security->get_csrf_hash()); $event->assign('csrfTokenName', $security->get_csrf_token_name()); $event->assign('siteURL', $config->getConfig('web')->get('base_url')); + $event->assign('serverName', $config->getConfig('web')->get('serverName')); Logger::logInfo("Assigned variables to TemplateEngine from WebComponent"); } From 7c77c339cf25994bc89f8efcaa6259b43d132dd5 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Mon, 22 Jul 2019 19:53:18 +0200 Subject: [PATCH 5/9] Implemented Resources. Resources are static files which have to be served by FuzeWorks. The developer can register a directory in which static files can be found, which shall be loaded if the folder is requested. This system should be avoided for high-performance applications. It is recommended to make special configurations in the web server in those kinds of cases. --- src/FuzeWorks/Event/ResourceServeEvent.php | 63 +++++++ src/FuzeWorks/Event/RouteWebRequestEvent.php | 2 +- src/FuzeWorks/Resources.php | 177 +++++++++++++++++++ src/FuzeWorks/WebComponent.php | 11 +- 4 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 src/FuzeWorks/Event/ResourceServeEvent.php create mode 100644 src/FuzeWorks/Resources.php diff --git a/src/FuzeWorks/Event/ResourceServeEvent.php b/src/FuzeWorks/Event/ResourceServeEvent.php new file mode 100644 index 0000000..109dc51 --- /dev/null +++ b/src/FuzeWorks/Event/ResourceServeEvent.php @@ -0,0 +1,63 @@ +resourceName = $resourceName; + $this->resourceUrlSegments = $resourceUrlSegments; + $this->resourceFilePath = $resourceFilePath; + } +} \ No newline at end of file diff --git a/src/FuzeWorks/Event/RouteWebRequestEvent.php b/src/FuzeWorks/Event/RouteWebRequestEvent.php index 58ac9f1..e99171a 100644 --- a/src/FuzeWorks/Event/RouteWebRequestEvent.php +++ b/src/FuzeWorks/Event/RouteWebRequestEvent.php @@ -1,6 +1,6 @@ output = Factory::getInstance()->output; + } + + public function resourceExists(array $resourceUrlSegments): bool + { + // First find the resource + $file = $this->findResource($resourceUrlSegments); + + // If not found, return false; + if (is_null($file)) + return false; + + // If found, simply return true + return true; + } + + /** + * Serves a static file if found. + * + * @param array $resourceUrlSegments + * @return bool + * @throws WebException + * + * @todo Bypass the Output system and use the readFile() method. + * @todo Run as FuzeWorks pre-code, before creating the container + */ + public function serveResource(array $resourceUrlSegments): bool + { + // First find the resource + $file = $this->findResource($resourceUrlSegments); + + // If not found return false + if (is_null($file)) + return false; + + // If a file is found, fire a serveResourceEvent + /** @var ResourceServeEvent $event */ + try { + $event = Events::fireEvent('resourceServeEvent', $file['resourceName'], $file['segments'], $file['file']); + } catch (Exception\EventException $e) { + throw new WebException("Could not serve resource. resourceServeEvent threw exception: '" . $e->getMessage() . "'"); + } + + // If cancelled, don't serve + if ($event->isCancelled()) + return false; + + // Log the resource serving + Logger::log("Serving static resource '/" . $file['resourceName'] . '/' . implode('/', $file['segments']) . "'"); + + // Serve file in accordance with event + $fileExtension = pathinfo($event->resourceFilePath, PATHINFO_EXTENSION); + $this->output->setContentType($fileExtension); + $this->output->setOutput(file_get_contents($event->resourceFilePath)); + #readfile($event->resourceFilePath); + + // And return true at the end + return true; + } + + protected function findResource(array $resourceUrlSegments): ?array + { + // If too few segments provided, don't even bother + if (count($resourceUrlSegments) < 2) + return null; + + // First segment should be the resourceName, check if it exists + $resourceName = urldecode($resourceUrlSegments[1]); + if (!isset($this->resources[$resourceName])) + return null; + + // If resource is found, generate file path + $resourceUrlSegmentsBck = $resourceUrlSegments; + array_shift($resourceUrlSegments); + $file = $this->resources[$resourceName] . DS . implode(DS, $resourceUrlSegments); + + // Test if file exists, if it does, return the string + if (file_exists($file) && is_file($file)) + return ['file' => $file, 'resourceName' => $resourceName, 'segments' => $resourceUrlSegments]; + + return null; + } + + /** + * Register a resource which can be served statically. + * + * The resourceName will be the directory under which the files shall be served on the web server. + * The filePath is where FuzeWorks should look for the files. + * + * @param string $resourceName + * @param string $filePath + * @throws WebException + * @return bool + */ + public function registerResources(string $resourceName, string $filePath): bool + { + // First check if the resource already exists + $resourceName = urldecode($resourceName); + if (isset($this->resources[$resourceName])) + throw new WebException("Could not register resources. Resources with same name already exists."); + + // Also check if the file path exists and is a directory + if (!file_exists($filePath) && !is_dir($filePath)) + throw new WebException("Could not register resources. Provided filePath does not exist."); + + // Add the resource + $this->resources[$resourceName] = $filePath; + + // Log the registration + Logger::log("Adding static resources on: '/" . $resourceName . "'"); + + return true; + } + + + +} \ No newline at end of file diff --git a/src/FuzeWorks/WebComponent.php b/src/FuzeWorks/WebComponent.php index 2a7e762..5915df1 100644 --- a/src/FuzeWorks/WebComponent.php +++ b/src/FuzeWorks/WebComponent.php @@ -39,6 +39,7 @@ namespace FuzeWorks; use FuzeWorks\Event\HaltExecutionEvent; use FuzeWorks\Event\LayoutLoadEvent; use FuzeWorks\Event\RouterCallViewEvent; +use FuzeWorks\Event\RouteWebRequestEvent; use FuzeWorks\Exception\ConfigException; use FuzeWorks\Exception\CSRFException; use FuzeWorks\Exception\EventException; @@ -72,6 +73,7 @@ class WebComponent implements iComponent 'input' => '\FuzeWorks\Input', 'output' => '\FuzeWorks\Output', 'uri' => '\FuzeWorks\URI', + 'resources' => '\FuzeWorks\Resources' ]; } @@ -167,10 +169,12 @@ class WebComponent implements iComponent /** @var URI $uri */ /** @var Output $output */ /** @var Security $security */ + /** @var Resources $resources */ $router = Factory::getInstance()->router; $uri = Factory::getInstance()->uri; $output = Factory::getInstance()->output; $security = Factory::getInstance()->security; + $resources = Factory::getInstance()->resources; // And start logging the request Logger::newLevel("Routing web request..."); @@ -181,10 +185,15 @@ class WebComponent implements iComponent return true; // Send webRequestEvent, if no cache is found + /** @var RouteWebRequestEvent $event */ $event = Events::fireEvent('routeWebRequestEvent', $uriString); if ($event->isCancelled()) return true; + // Attempt to load a static resource + if ($resources->serveResource($uri->segmentArray())) + return true; + // First test for Cross Site Request Forgery try { $security->csrf_verify(); @@ -206,7 +215,7 @@ class WebComponent implements iComponent // Remove listener so that error pages won't be intercepted Events::removeListener([$this, 'callViewEventListener'], 'routerCallViewEvent',Priority::HIGHEST); - // Request 404 page= + // Request 404 page try { $viewOutput = $router->route('Error/error404'); } catch (NotFoundException $e) { From fd531b53e20fa309df0e718656f06eb8c7a17ea6 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Wed, 21 Aug 2019 19:49:45 +0200 Subject: [PATCH 6/9] Released 1.2.0-RC4 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 012f6d5..6156205 100644 --- a/composer.json +++ b/composer.json @@ -14,8 +14,8 @@ ], "require": { "php": ">=7.1.0", - "fuzeworks/mvcr": "1.2.0-RC3", - "fuzeworks/core": "1.2.0-RC3" + "fuzeworks/mvcr": "1.2.0-RC4", + "fuzeworks/core": "1.2.0-RC4" }, "minimum-stability": "RC", "prefer-stable": true, From a04293d8b4749f7ddb53baf44ffbff1ed907e076 Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Tue, 17 Sep 2019 22:13:56 +0200 Subject: [PATCH 7/9] Fixed bug resulting in exception on every request. --- src/FuzeWorks/URI.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FuzeWorks/URI.php b/src/FuzeWorks/URI.php index 29fa54b..610dc6c 100644 --- a/src/FuzeWorks/URI.php +++ b/src/FuzeWorks/URI.php @@ -60,8 +60,8 @@ class URI private $input; protected $baseUri; - protected $uriString; - protected $segments; + protected $uriString = ''; + protected $segments = []; public function init() { From 35df98820069e4fd5adc800f397174f81c12e18b Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Wed, 18 Sep 2019 00:21:03 +0200 Subject: [PATCH 8/9] Release 1.2.0-RC5 --- composer.json | 4 ++-- src/FuzeWorks/WebComponent.php | 29 ++++++++++++++++++++++------- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 6156205..d7fe6fe 100644 --- a/composer.json +++ b/composer.json @@ -14,8 +14,8 @@ ], "require": { "php": ">=7.1.0", - "fuzeworks/mvcr": "1.2.0-RC4", - "fuzeworks/core": "1.2.0-RC4" + "fuzeworks/mvcr": "1.2.0-RC5", + "fuzeworks/core": "1.2.0-RC5" }, "minimum-stability": "RC", "prefer-stable": true, diff --git a/src/FuzeWorks/WebComponent.php b/src/FuzeWorks/WebComponent.php index 5915df1..0db2716 100644 --- a/src/FuzeWorks/WebComponent.php +++ b/src/FuzeWorks/WebComponent.php @@ -39,11 +39,12 @@ namespace FuzeWorks; use FuzeWorks\Event\HaltExecutionEvent; use FuzeWorks\Event\LayoutLoadEvent; use FuzeWorks\Event\RouterCallViewEvent; +use FuzeWorks\Event\RouterLoadViewAndControllerEvent; use FuzeWorks\Event\RouteWebRequestEvent; use FuzeWorks\Exception\ConfigException; -use FuzeWorks\Exception\CSRFException; use FuzeWorks\Exception\EventException; use FuzeWorks\Exception\Exception; +use FuzeWorks\Exception\FactoryException; use FuzeWorks\Exception\HaltException; use FuzeWorks\Exception\NotFoundException; use FuzeWorks\Exception\OutputException; @@ -140,6 +141,7 @@ class WebComponent implements iComponent * @throws RouterException * @throws WebException * @throws EventException + * @throws FactoryException */ public function routeWebRequest(): bool { @@ -156,6 +158,17 @@ class WebComponent implements iComponent return $event; }, 'coreShutdownEvent', Priority::NORMAL); + // Add HTTP method prefix to requests to views + Events::addListener(function($event){ + /** @var Input $input */ + /** @var RouterLoadViewAndControllerEvent $event */ + $input = Factory::getInstance('input'); + $methods = $event->viewMethods[Priority::NORMAL]; + foreach ($methods as $method) + $event->addMethod(strtolower($input->method()) . '_' . $method); + return $event; + }, 'routerLoadViewAndControllerEvent', Priority::NORMAL); + // Create an error 500 page when a haltEvent is fired Events::addListener([$this, 'haltEventListener'], 'haltExecutionEvent', Priority::NORMAL); } catch (EventException $e) { @@ -170,11 +183,11 @@ class WebComponent implements iComponent /** @var Output $output */ /** @var Security $security */ /** @var Resources $resources */ - $router = Factory::getInstance()->router; - $uri = Factory::getInstance()->uri; - $output = Factory::getInstance()->output; - $security = Factory::getInstance()->security; - $resources = Factory::getInstance()->resources; + $router = Factory::getInstance('router'); + $uri = Factory::getInstance('uri'); + $output = Factory::getInstance('output'); + $security = Factory::getInstance('security'); + $resources = Factory::getInstance('resources'); // And start logging the request Logger::newLevel("Routing web request..."); @@ -207,7 +220,7 @@ class WebComponent implements iComponent // Attempt to load the requested page try { - $viewOutput = $router->route($uriString); + $viewOutput = $router->route($event->uriString); } catch (NotFoundException $e) { Logger::logWarning("Requested page not found. Requesting Error/error404 View"); $output->setStatusHeader(404); @@ -280,6 +293,7 @@ class WebComponent implements iComponent * * @param $event * @throws EventException + * @throws FactoryException * @TODO remove FuzeWorks\Layout dependency */ public function haltEventListener(HaltExecutionEvent $event) @@ -323,6 +337,7 @@ class WebComponent implements iComponent * * @param LayoutLoadEvent $event * @throws ConfigException + * @throws FactoryException */ public function layoutLoadEventListener($event) { From ddbc787c45bbb70312c13f9585a0069c079b3ddf Mon Sep 17 00:00:00 2001 From: Abel Hoogeveen Date: Sat, 21 Sep 2019 20:28:03 +0200 Subject: [PATCH 9/9] Release 1.2.0 --- composer.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index d7fe6fe..387cf9d 100644 --- a/composer.json +++ b/composer.json @@ -14,11 +14,9 @@ ], "require": { "php": ">=7.1.0", - "fuzeworks/mvcr": "1.2.0-RC5", - "fuzeworks/core": "1.2.0-RC5" + "fuzeworks/mvcr": "~1.2.0", + "fuzeworks/core": "~1.2.0" }, - "minimum-stability": "RC", - "prefer-stable": true, "require-dev": { "phpunit/phpunit": "^7" }, @@ -27,5 +25,4 @@ "FuzeWorks\\": "src/FuzeWorks/" } } - } \ No newline at end of file