Merge branch 'development' into 'master'

Release 1.2.0

See merge request fuzeworks/webcomponent!2
This commit is contained in:
Abel Hoogeveen 2019-09-21 18:28:47 +00:00
commit 0ba98f8130
14 changed files with 934 additions and 329 deletions

View File

@ -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/"
}
}
}

View File

@ -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
];

View File

@ -0,0 +1,63 @@
<?php
/**
* FuzeWorks WebComponent.
*
* The FuzeWorks PHP FrameWork
*
* Copyright (C) 2013-2019 TechFuze
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net)
* @license https://opensource.org/licenses/MIT MIT License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.2.0
*
* @version Version 1.2.0
*/
namespace FuzeWorks\Event;
use FuzeWorks\Event;
class ResourceServeEvent extends Event
{
/**
* @var string
*/
public $resourceName;
/**
* @var array
*/
public $resourceUrlSegments;
/**
* @var string
*/
public $resourceFilePath;
public function init(string $resourceName, array $resourceUrlSegments, string $resourceFilePath)
{
$this->resourceName = $resourceName;
$this->resourceUrlSegments = $resourceUrlSegments;
$this->resourceFilePath = $resourceFilePath;
}
}

View File

@ -0,0 +1,53 @@
<?php
/**
* FuzeWorks WebComponent.
*
* The FuzeWorks PHP FrameWork
*
* Copyright (C) 2013-2019 TechFuze
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net)
* @license https://opensource.org/licenses/MIT MIT License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.2.0
*
* @version Version 1.2.0
*/
namespace FuzeWorks\Event;
use FuzeWorks\Event;
class RouteWebRequestEvent extends Event
{
/**
* @var string
*/
public $uriString;
public function init(string $uriString)
{
$this->uriString = $uriString;
}
}

View File

@ -0,0 +1,42 @@
<?php
/**
* FuzeWorks Component.
*
* The FuzeWorks PHP FrameWork
*
* Copyright (C) 2013-2019 TechFuze
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net)
* @license https://opensource.org/licenses/MIT MIT License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.2.0
*
* @version Version 1.2.0
*/
namespace FuzeWorks\Exception;
class CSRFException extends SecurityException
{
}

View File

@ -1,4 +1,4 @@
<?php /** @noinspection ALL */
<?php
/**
* FuzeWorks WebComponent.
@ -38,8 +38,10 @@
namespace FuzeWorks;
use FuzeWorks\ConfigORM\ConfigORM;
use Tracy\Debugger;
/**
* @todo Implement remaining methods from OldInput
*/
class Input
{
/**
@ -69,6 +71,13 @@ class Input
// Set the configuration
$this->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;
}
}

View File

@ -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)

177
src/FuzeWorks/Resources.php Normal file
View File

@ -0,0 +1,177 @@
<?php
/**
* FuzeWorks WebComponent.
*
* The FuzeWorks PHP FrameWork
*
* Copyright (C) 2013-2019 TechFuze
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* @author TechFuze
* @copyright Copyright (c) 2013 - 2019, TechFuze. (http://techfuze.net)
* @license https://opensource.org/licenses/MIT MIT License
*
* @link http://techfuze.net/fuzeworks
* @since Version 1.2.0
*
* @version Version 1.2.0
*/
namespace FuzeWorks;
use FuzeWorks\Event\ResourceServeEvent;
use FuzeWorks\Exception\WebException;
/**
* FuzeWorks' handler for static resources.
*
* Objects in FuzeWorks can register a folder with static resources, which shall be served if requested by clients.
* 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.
*/
class Resources
{
private $resources = [];
/**
* @var Output
*/
private $output;
public function init()
{
$this->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;
}
}

View File

@ -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(
'#<img[\s/]+.*?src\s*=\s*(["\'])([^\\1]+?)\\1.*?\>#i',
'#<img[\s/]+.*?src\s*=\s*?(([^\s"\'=<>`]+)).*?\>#i'
),
'\\2',
$str
);
}
// ----------------------------------------------------------------
/**

View File

@ -60,18 +60,16 @@ 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;
}
}

View File

@ -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 (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);
$output->setStatusHeader(500);
try {
$viewOutput = $router->route('Error/error500');
} catch (Exception $error500Exception) {
Logger::exceptionHandler($error500Exception, false);
$viewOutput = 'ERROR 500. Page could not be loaded.';
$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");
}
}

View File

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

View File

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

View File

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