diff --git a/composer.json b/composer.json
index d630e82..387cf9d 100644
--- a/composer.json
+++ b/composer.json
@@ -14,17 +14,15 @@
],
"require": {
"php": ">=7.1.0",
- "fuzeworks/core": "dev-development",
- "fuzeworks/mvcr": "dev-master"
+ "fuzeworks/mvcr": "~1.2.0",
+ "fuzeworks/core": "~1.2.0"
},
"require-dev": {
- "phpunit/phpunit": "^7",
- "fuzeworks/tracycomponent": "dev-master"
+ "phpunit/phpunit": "^7"
},
"autoload": {
"psr-4": {
"FuzeWorks\\": "src/FuzeWorks/"
}
}
-
}
\ No newline at end of file
diff --git a/src/Config/config.security.php b/src/Config/config.security.php
index 891cda4..346fcbc 100644
--- a/src/Config/config.security.php
+++ b/src/Config/config.security.php
@@ -49,56 +49,16 @@ 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' => ''
+ // 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/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
new file mode 100644
index 0000000..e99171a
--- /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/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 = Factory::getInstance()->config->getConfig('web');
+ // If not handling requests, do not continue
+ if (!WebComponent::$willHandleRequest)
+ return;
+
+ // Start session
+ session_start();
+
// Sanitize all global arrays
$this->sanitizeGlobals();
@@ -76,8 +85,8 @@ class Input
{
if (class_exists('\FuzeWorks\TracyComponent', true) && \FuzeWorks\TracyComponent::isEnabled())
{
- set_exception_handler([$this, 'tracyExceptionHandler']);
- set_error_handler([$this, 'tracyErrorHandler']);
+ Core::addExceptionHandler([$this, 'restoreGlobalArrays'], Priority::HIGHEST);
+ Core::addErrorHandler([$this, 'restoreGlobalArrays'], Priority::HIGHEST);
}
Events::addListener(
[$this, 'restoreGlobalArrays'],
@@ -87,35 +96,10 @@ class Input
}
/**
- * Used to restore global arrays before handling errors by Tracy
+ * Restores global arrays before handling by processes outside of FuzeWorks
*
- * @param $exception
- * @param bool $exit
* @internal
*/
- public function tracyExceptionHandler($exception, $exit = true)
- {
- $this->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);
- }
-
public function restoreGlobalArrays()
{
Logger::logInfo('Restoring global $_GET, $_POST, $_SERVER, $_COOKIE arrays');
@@ -235,6 +219,8 @@ class Input
}
/**
+ * Used to fetch variables from the global arrays
+ *
* @param string $arrayName
* @param null $index
* @param bool $xssClean
@@ -242,8 +228,8 @@ class Input
*/
protected function getFromInputArray(string $arrayName, $index = null, bool $xssClean = true)
{
- // Clean XSS if requested manually or forced through configuration
- $xssClean = $xssClean || $this->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]));
@@ -266,59 +252,124 @@ 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);
}
+ /**
+ * 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 16797c9..a623f14 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
{
@@ -51,6 +53,13 @@ class Output
*/
private $input;
+ /**
+ * The internal URI class
+ *
+ * @var URI
+ */
+ private $uri;
+
/**
* WebCfg
*
@@ -82,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');
@@ -95,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
*
@@ -132,45 +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');
- }
-
- // 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
*
@@ -181,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];
}
@@ -214,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/Resources.php b/src/FuzeWorks/Resources.php
new file mode 100644
index 0000000..4dd39eb
--- /dev/null
+++ b/src/FuzeWorks/Resources.php
@@ -0,0 +1,177 @@
+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/Security.php b/src/FuzeWorks/Security.php
index d42036c..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
@@ -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 {
@@ -168,6 +169,13 @@ class Security {
*/
private $config;
+ /**
+ * Input. A dependency for this class
+ *
+ * @var Input
+ */
+ private $input;
+
/**
* Class constructor
*
@@ -177,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)
@@ -200,7 +209,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'));
}
// --------------------------------------------------------------------
@@ -209,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)
@@ -231,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();
@@ -257,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
*
@@ -266,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;
}
// --------------------------------------------------------------------
@@ -292,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);
}
// --------------------------------------------------------------------
@@ -727,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 1763575..610dc6c 100644
--- a/src/FuzeWorks/URI.php
+++ b/src/FuzeWorks/URI.php
@@ -60,17 +60,15 @@ class URI
private $input;
protected $baseUri;
- protected $uriString;
- protected $segments;
+ protected $uriString = '';
+ protected $segments = [];
public function init()
{
$this->input = Factory::getInstance()->input;
$this->config = Factory::getInstance()->config->getConfig('web');
if (WebComponent::$willHandleRequest)
- {
$this->determineUri();
- }
}
public function determineUri()
@@ -86,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)));
}
@@ -224,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 451ec8e..0db2716 100644
--- a/src/FuzeWorks/WebComponent.php
+++ b/src/FuzeWorks/WebComponent.php
@@ -36,14 +36,24 @@
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\EventException;
use FuzeWorks\Exception\Exception;
+use FuzeWorks\Exception\FactoryException;
+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
{
-
/**
* Whether WebComponent is configured to handle a web request
*
@@ -64,13 +74,10 @@ class WebComponent implements iComponent
'input' => '\FuzeWorks\Input',
'output' => '\FuzeWorks\Output',
'uri' => '\FuzeWorks\URI',
+ 'resources' => '\FuzeWorks\Resources'
];
}
- /**
- * @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,60 +102,133 @@ 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();
+
+ // Register some base events
+ Events::addListener([$this, 'layoutLoadEventListener'], 'layoutLoadEvent', Priority::NORMAL);
}
+ /**
+ * 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 OutputException
+ * @throws RouterException
* @throws WebException
+ * @throws EventException
+ * @throws FactoryException
*/
public function routeWebRequest(): bool
{
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 {
- Events::addListener(function () {
+ // Set the output to display when shutting down
+ Events::addListener(function ($event) {
/** @var Output $output */
Logger::logInfo("Parsing output...");
$output = Factory::getInstance()->output;
$output->display();
+ 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) {
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 */
- $router = Factory::getInstance()->router;
-
- /** @var URI $uriObject */
- $uriObject = Factory::getInstance()->uri;
- $uri = $uriObject->uriString();
-
+ /** @var URI $uri */
/** @var Output $output */
- $output = Factory::getInstance()->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...");
+
+ // First check if a cached page is available
+ $uriString = $uri->uriString();
+ if ($output->getCache($uriString))
+ 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();
+ } 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 {
- $viewOutput = $router->route($uri);
+ $viewOutput = $router->route($event->uriString);
} catch (NotFoundException $e) {
Logger::logWarning("Requested page not found. Requesting Error/error404 View");
$output->setStatusHeader(404);
- // Request 404 page=
+ // 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');
} catch (NotFoundException $e) {
@@ -158,14 +238,21 @@ 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);
+ } 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/error500');
- } catch (Exception $error500Exception) {
- Logger::exceptionHandler($error500Exception, false);
- $viewOutput = 'ERROR 500. Page could not be loaded.';
+ $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.';
}
}
@@ -173,6 +260,99 @@ class WebComponent implements iComponent
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
+ *
+ * Fired when FuzeWorks halts it's execution. Loads an error 500 page.
+ *
+ * @param $event
+ * @throws EventException
+ * @throws FactoryException
+ * @TODO remove FuzeWorks\Layout dependency
+ */
+ public function haltEventListener(HaltExecutionEvent $event)
+ {
+ // Dependencies
+ /** @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);
+
+ 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);
+ }
+
+ /**
+ * Listener for layoutLoadEvent
+ *
+ * Assigns variables from the WebComponent to Layout engines.
+ *
+ * @param LayoutLoadEvent $event
+ * @throws ConfigException
+ * @throws FactoryException
+ */
+ public function layoutLoadEventListener($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'));
+ $event->assign('serverName', $config->getConfig('web')->get('serverName'));
+
+ Logger::logInfo("Assigned variables to TemplateEngine from WebComponent");
+ }
}
\ 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