diff --git a/README.md b/README.md index f416e0d..98aa57b 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,31 @@ Simple, fast and yet powerful PHP router that is easy to get integrated and in a The goal of this project is to create a router that is more or less 100% compatible with the Laravel documentation, while remaining as simple as possible, and as easy to integrate and change without compromising either speed or complexity. Being lightweight is the #1 priority. -### Ideas and issues +### Feedback and development -If you want a great new feature or experience any issues what-so-ever, please feel free to leave an issue and i'll look into it whenever possible. +If you are missing a feature, experience problems or have ideas or feedback that you want us to hear, please feel free to create an issue. + +###### Issues guidelines + +- Please be as detailed as possible in the description when creating a new issue. This will help others to more easily understand- and solve your issue. +For example: if you are experiencing issues, you should provide the necessary steps to reproduce the error within your description. + +- We love to hear out any ideas or feedback to the library. + +[Create a new issue here](https://github.com/skipperbent/simple-php-router/issues/new) + +###### Contribution development guidelines + +- Please try to follow the PSR-2 codestyle guidelines. + +- Please create your pull requests to the development base that matches the version number you want to change. +For example when pushing changes to version 3, the pull request should use the `v3-development` base/branch. + +- Create detailed descriptions for your commits, as these will be used in the changelog for new releases. + +- When changing existing functionality, please ensure that the unit-tests working. + +- When adding new stuff, please remember to add new unit-tests for the functionality. --- @@ -51,6 +73,8 @@ If you want a great new feature or experience any issues what-so-ever, please fe - [CSRF-protection](#csrf-protection) - [Adding CSRF-verifier](#adding-csrf-verifier) - [Getting CSRF-token](#getting-csrf-token) + - [Custom CSRF-verifier](#custom-csrf-verifier) + - [Custom Token-provider](#custom-token-provider) - [Middlewares](#middlewares) - [Example](#example) @@ -215,13 +239,13 @@ Simply create a new `web.config` file in your projects `public` directory and pa #### Troubleshoting If you do not have a favicon.ico file in your project, you can get `404 Router::notFoundException()` constantly. -To add `favicon.ico` as exception, you can add this line to the `` group: +To add `favicon.ico` as exception, you can add this line to the `` group: `````` -You can also make one exception for files with some extensions: +You can also make one exception for files with some extensions: `````` -If you are using `$_SERVER['ORIG_PATH_INFO']`, you will get `\index.php\` as part of the returned value. By sample: +If you are using `$_SERVER['ORIG_PATH_INFO']`, you will get `\index.php\` as part of the returned value. By sample: ```/index.php/test/mypage.php``` ### Configuration @@ -681,11 +705,27 @@ SimpleRouter::get('/page/404', 'ControllerPage@notFound', ['as' => 'page.notfoun # CSRF Protection -Any forms posting to `POST`, `PUT` or `DELETE` routes should include the CSRF-token. We strongly recommend that you create your enable CSRF-verification on your site. +Any forms posting to `POST`, `PUT` or `DELETE` routes should include the CSRF-token. We strongly recommend that you enable CSRF-verification on your site to maximize security. -Create a new class and extend the ```BaseCsrfVerifier``` middleware class provided with simple-php-router. +You can use the `BaseCsrfVerifier` to enable CSRF-validation on all request. If you need to disable verification for specific urls, please refer to the "Custom CSRF-verifier" section below. -Add the property ```except``` with an array of the urls to the routes you would like to exclude/whitelist from the CSRF validation. Using ```*``` at the end for the url will match the entire url. +By default simple-php-router will use the `CookieTokenProvider` class. This provider will store the security-token in a cookie on the clients machine. +If you want to store the token elsewhere, please refer to the "Creating custom Token Provider" section below. + +## Adding CSRF-verifier + +When you've created your CSRF-verifier you need to tell simple-php-router that it should use it. You can do this by adding the following line in your `routes.php` file: + +```php +Router::csrfVerifier(new \Demo\Middlewares\CsrfVerifier()); +``` + +## Custom CSRF-verifier + +Create a new class and extend the `BaseCsrfVerifier` middleware class provided by default with the simple-php-router library. + +Add the property `except` with an array of the urls to the routes you want to exclude/whitelist from the CSRF validation. +Using ```*``` at the end for the url will match the entire url. **Here's a basic example on a CSRF-verifier class:** @@ -703,12 +743,45 @@ class CsrfVerifier extends BaseCsrfVerifier } ``` -## Adding CSRF-verifier +## Custom Token Provider -When you've created your CSRF verifier - you need to tell simple-php-router that it should use it. You can do this by adding the following line in your `routes.php` file: +By default the `BaseCsrfVerifier` will use the `CookieTokenProvider` to store the token in a cookie on the clients machine. + +If you need to store the token elsewhere, you can do that by creating your own class and implementing the `ITokenProvider` class. ```php -Router::csrfVerifier(new \Demo\Middlewares\CsrfVerifier()); +class SessionTokenProvider implements ITokenProvider +{ + + /** + * Refresh existing token + */ + public function refresh() + { + // Implement your own functionality here... + } + + /** + * Validate valid CSRF token + * + * @param string $token + * @return bool + */ + public function validate($token) + { + // Implement your own functionality here... + } + +} +``` + +Next you need to set your custom `ITokenProvider` implementation on your `BaseCsrfVerifier` class in your routes file: + +```php +$verifier = new \dscuz\Middleware\CsrfVerifier(); +$verifier->setTokenProvider(new SessionTokenProvider()); + +Router::csrfVerifier($verifier); ``` ## Getting CSRF-token @@ -721,6 +794,12 @@ You can get the CSRF-token by calling the helper method: csrf_token(); ``` +You can also get the token directly: + +```php +return Router::router()->getCsrfVerifier()->getTokenProvider()->getToken(); +``` + The default name/key for the input-field is `csrf_token` and is defined in the `POST_KEY` constant in the `BaseCsrfVerifier` class. You can change the key by overwriting the constant in your own CSRF-verifier class. @@ -1028,6 +1107,7 @@ All object implements the `IInputItem` interface and will always contain these m - `getValue()` - returns the value of the input. `InputFile` has the same methods as above along with some other file-specific methods like: +- `getFilename` - get the filename. - `getTmpName()` - get file temporary name. - `getSize()` - get file size. - `move($destination)` - move file to destination. @@ -1051,7 +1131,7 @@ $siteId = input('site_id', 2, ['post', 'get']); ## Url rewriting Sometimes it can be useful to manipulate the route about to be loaded. simple-php-router allows you to easily change the route about to be executed. -All information about the current route is stored in the ```\Pecee\SimpleRouter\Router``` instance's `loadedRoute` property. +All information about the current route is stored in the `\Pecee\SimpleRouter\Router` instance's `loadedRoute` property. For easy access you can use the shortcut method `\Pecee\SimpleRouter\SimpleRouter::router()`. diff --git a/helpers.php b/helpers.php index 2c5c1c4..93d4e32 100644 --- a/helpers.php +++ b/helpers.php @@ -73,7 +73,7 @@ function csrf_token() { $baseVerifier = Router::router()->getCsrfVerifier(); if ($baseVerifier !== null) { - return $baseVerifier->getToken(); + return $baseVerifier->getTokenProvider()->getToken(); } return null; diff --git a/src/Pecee/Http/Input/Input.php b/src/Pecee/Http/Input/Input.php index e34d05c..1ced091 100644 --- a/src/Pecee/Http/Input/Input.php +++ b/src/Pecee/Http/Input/Input.php @@ -35,7 +35,7 @@ class Input public function parseInputs() { /* Parse get requests */ - if (count($_GET) > 0) { + if (count($_GET) !== 0) { $this->get = $this->handleGetPost($_GET); } @@ -46,12 +46,12 @@ class Input parse_str(file_get_contents('php://input'), $postVars); } - if (count($postVars) > 0) { + if (count($postVars) !== 0) { $this->post = $this->handleGetPost($postVars); } /* Parse get requests */ - if (count($_FILES) > 0) { + if (count($_FILES) !== 0) { $this->file = $this->parseFiles(); } } @@ -69,7 +69,7 @@ class Input continue; } - $keys = []; + $keys = [$key]; $files = $this->rearrangeFiles($value['name'], $keys, $value); @@ -87,6 +87,9 @@ class Input protected function rearrangeFiles(array $values, &$index, $original) { + $originalIndex = $index[0]; + array_shift($index); + $output = []; $getItem = function ($key, $property = 'name') use ($original, $index) { @@ -107,7 +110,7 @@ class Input if (is_array($getItem($key)) === false) { $file = InputFile::createFromArray([ - 'index' => $key, + 'index' => (empty($key) === true && empty($originalIndex) === false) ? $originalIndex : $key, 'filename' => $getItem($key), 'error' => $getItem($key, 'error'), 'tmp_name' => $getItem($key, 'tmp_name'), @@ -128,7 +131,7 @@ class Input $files = $this->rearrangeFiles($value, $index, $original); - if (isset($output[$key])) { + if (isset($output[$key]) === true) { $output[$key][] = $files; } else { $output[$key] = $files; @@ -217,15 +220,15 @@ class Input $element = null; - if ($methods === null || in_array('get', $methods)) { + if ($methods === null || in_array('get', $methods, false) === true) { $element = $this->findGet($index); } - if (($element === null && $methods === null) || ($methods !== null && in_array('post', $methods))) { + if (($element === null && $methods === null) || ($methods !== null && in_array('post', $methods, false) === true)) { $element = $this->findPost($index); } - if (($element === null && $methods === null) || ($methods !== null && in_array('file', $methods))) { + if (($element === null && $methods === null) || ($methods !== null && in_array('file', $methods, false) === true)) { $element = $this->findFile($index); } diff --git a/src/Pecee/Http/Input/InputFile.php b/src/Pecee/Http/Input/InputFile.php index ac78ecf..c65eb1b 100644 --- a/src/Pecee/Http/Input/InputFile.php +++ b/src/Pecee/Http/Input/InputFile.php @@ -16,7 +16,7 @@ class InputFile implements IInputItem $this->index = $index; // Make the name human friendly, by replace _ with space - $this->name = ucfirst(str_replace('_', ' ', $this->index)); + $this->name = ucfirst(str_replace('_', ' ', strtolower($this->index))); } /** @@ -28,7 +28,7 @@ class InputFile implements IInputItem */ public static function createFromArray(array $values) { - if (!isset($values['index'])) { + if (array_key_exists('index', $values) === false) { throw new \InvalidArgumentException('Index key is required'); } @@ -39,6 +39,7 @@ class InputFile implements IInputItem 'type' => null, 'size' => null, 'name' => null, + 'filename' => null, 'error' => null, ], $values); @@ -47,7 +48,7 @@ class InputFile implements IInputItem ->setError($values['error']) ->setType($values['type']) ->setTmpName($values['tmp_name']) - ->setFilename($values['name']); + ->setFilename($values['filename']); } @@ -267,8 +268,9 @@ class InputFile implements IInputItem 'tmp_name' => $this->tmpName, 'type' => $this->type, 'size' => $this->size, - 'name' => $this->filename, + 'name' => $this->name, 'error' => $this->error, + 'filename' => $this->filename, ]; } diff --git a/src/Pecee/Http/Input/InputItem.php b/src/Pecee/Http/Input/InputItem.php index 8554088..2128c3a 100644 --- a/src/Pecee/Http/Input/InputItem.php +++ b/src/Pecee/Http/Input/InputItem.php @@ -13,7 +13,7 @@ class InputItem implements IInputItem $this->value = $value; // Make the name human friendly, by replace _ with space - $this->name = ucfirst(str_replace('_', ' ', $this->index)); + $this->name = ucfirst(str_replace('_', ' ', strtolower($this->index))); } /** diff --git a/src/Pecee/Http/Middleware/BaseCsrfVerifier.php b/src/Pecee/Http/Middleware/BaseCsrfVerifier.php index 7eb80d6..e0f166b 100644 --- a/src/Pecee/Http/Middleware/BaseCsrfVerifier.php +++ b/src/Pecee/Http/Middleware/BaseCsrfVerifier.php @@ -2,9 +2,10 @@ namespace Pecee\Http\Middleware; -use Pecee\CsrfToken; use Pecee\Http\Middleware\Exceptions\TokenMismatchException; use Pecee\Http\Request; +use Pecee\Http\Security\CookieTokenProvider; +use Pecee\Http\Security\ITokenProvider; class BaseCsrfVerifier implements IMiddleware { @@ -12,15 +13,11 @@ class BaseCsrfVerifier implements IMiddleware const HEADER_KEY = 'X-CSRF-TOKEN'; protected $except; - protected $csrfToken; - protected $token; + protected $tokenProvider; public function __construct() { - $this->csrfToken = new CsrfToken(); - - // Generate or get the CSRF-Token from Cookie. - $this->token = $this->csrfToken->getToken($this->generateToken()); + $this->tokenProvider = new CookieTokenProvider(); } /** @@ -30,7 +27,7 @@ class BaseCsrfVerifier implements IMiddleware */ protected function skip(Request $request) { - if ($this->except === null || is_array($this->except) === false) { + if ($this->except === null || count($this->except) === 0) { return false; } @@ -67,37 +64,29 @@ class BaseCsrfVerifier implements IMiddleware $token = $request->getHeader(static::HEADER_KEY); } - if ($this->csrfToken->validate($token) === false) { - throw new TokenMismatchException('Invalid csrf-token.'); + if ($this->tokenProvider->validate($token) === false) { + throw new TokenMismatchException('Invalid CSRF-token.'); } } // Refresh existing token - $this->csrfToken->refresh(); + $this->tokenProvider->refresh(); } - public function generateToken() + public function getTokenProvider() { - $token = CsrfToken::generateToken(); - $this->csrfToken->setToken($token); - - return $token; + return $this->tokenProvider; } - public function hasToken() + /** + * Set token provider + * @param ITokenProvider $provider + */ + public function setTokenProvider(ITokenProvider $provider) { - if ($this->token !== null) { - return true; - } - - return $this->csrfToken->hasToken(); - } - - public function getToken() - { - return $this->token; + $this->tokenProvider = $provider; } } \ No newline at end of file diff --git a/src/Pecee/CsrfToken.php b/src/Pecee/Http/Security/CookieTokenProvider.php similarity index 69% rename from src/Pecee/CsrfToken.php rename to src/Pecee/Http/Security/CookieTokenProvider.php index 87b47b5..c8e0115 100644 --- a/src/Pecee/CsrfToken.php +++ b/src/Pecee/Http/Security/CookieTokenProvider.php @@ -1,12 +1,22 @@ token = $this->getToken(); + + if ($this->token === null) { + $this->token = $this->generateToken(); + } + } /** * Generate random identifier for CSRF token @@ -14,7 +24,7 @@ class CsrfToken * @throws \RuntimeException * @return string */ - public static function generateToken() + public function generateToken() { if (function_exists('random_bytes') === true) { return bin2hex(random_bytes(32)); @@ -54,7 +64,7 @@ class CsrfToken public function setToken($token) { $this->token = $token; - setcookie(static::CSRF_KEY, $token, time() + 60 * 120, '/'); + setcookie(static::CSRF_KEY, $token, time() + 60 * $this->cookieTimeoutMinutes, '/'); } /** @@ -88,4 +98,22 @@ class CsrfToken return isset($_COOKIE[static::CSRF_KEY]); } + /** + * Get timeout for cookie in minutes + * @return int + */ + public function getCookieTimeoutMinutes() + { + return $this->cookieTimeoutMinutes; + } + + /** + * Set cookie timeout in minutes + * @param $minutes + */ + public function setCookieTimeoutMinutes($minutes) + { + $this->cookieTimeoutMinutes = $minutes; + } + } \ No newline at end of file diff --git a/src/Pecee/Http/Security/ITokenProvider.php b/src/Pecee/Http/Security/ITokenProvider.php new file mode 100644 index 0000000..6b1ee72 --- /dev/null +++ b/src/Pecee/Http/Security/ITokenProvider.php @@ -0,0 +1,21 @@ +getMiddlewares()); - if ($max > 0) { + if ($max !== 0) { for ($i = 0; $i < $max; $i++) { @@ -57,7 +57,7 @@ abstract class LoadableRoute extends Route implements ILoadableRoute return null; } - return (preg_match($this->regex, $request->getHost() . $url) > 0); + return (preg_match($this->regex, $request->getHost() . $url) !== 0); } /** @@ -74,7 +74,7 @@ abstract class LoadableRoute extends Route implements ILoadableRoute $regex = sprintf(static::PARAMETERS_REGEX_FORMAT, $this->paramModifiers[0], $this->paramOptionalSymbol, $this->paramModifiers[1]); - if (preg_match_all('/' . $regex . '/u', $this->url, $matches)) { + if (preg_match_all('/' . $regex . '/u', $this->url, $matches) === 1) { $this->parameters = array_fill_keys($matches[1], null); } } @@ -102,7 +102,7 @@ abstract class LoadableRoute extends Route implements ILoadableRoute $group = $this->getGroup(); - if ($group !== null && count($group->getDomains()) > 0) { + if ($group !== null && count($group->getDomains()) !== 0) { $url = '//' . $group->getDomains()[0] . $url; } diff --git a/src/Pecee/SimpleRouter/Route/Route.php b/src/Pecee/SimpleRouter/Route/Route.php index 1e04dde..cc6f40d 100644 --- a/src/Pecee/SimpleRouter/Route/Route.php +++ b/src/Pecee/SimpleRouter/Route/Route.php @@ -117,7 +117,7 @@ abstract class Route implements IRoute // Ensures that hostnames/domains will work with parameters $url = '/' . ltrim($url, '/'); - if (preg_match_all('/' . $regex . '/u', $route, $parameters)) { + if (preg_match_all('/' . $regex . '/u', $route, $parameters) !== 0) { $urlParts = preg_split('/((\-?\/?)\{[^}]+\})/', rtrim($route, '/')); @@ -155,7 +155,7 @@ abstract class Route implements IRoute $urlRegex = preg_quote($route, '/'); } - if (preg_match(sprintf($this->urlRegex, $urlRegex), $url, $matches) > 0) { + if (preg_match(sprintf($this->urlRegex, $urlRegex), $url, $matches) === 1) { $values = []; @@ -361,15 +361,15 @@ abstract class Route implements IRoute $values['namespace'] = $this->namespace; } - if (count($this->requestMethods) > 0) { + if (count($this->requestMethods) !== 0) { $values['method'] = $this->requestMethods; } - if (count($this->where) > 0) { + if (count($this->where) !== 0) { $values['where'] = $this->where; } - if (count($this->middlewares) > 0) { + if (count($this->middlewares) !== 0) { $values['middleware'] = $this->middlewares; } @@ -389,28 +389,28 @@ abstract class Route implements IRoute */ public function setSettings(array $values, $merge = false) { - if ($this->namespace === null && isset($values['namespace'])) { + if ($this->namespace === null && isset($values['namespace']) === true) { $this->setNamespace($values['namespace']); } - if (isset($values['method'])) { + if (isset($values['method']) === true) { $this->setRequestMethods(array_merge($this->requestMethods, (array)$values['method'])); } - if (isset($values['where'])) { + if (isset($values['where']) === true) { $this->setWhere(array_merge($this->where, (array)$values['where'])); } - if (isset($values['parameters'])) { + if (isset($values['parameters']) === true) { $this->setParameters(array_merge($this->parameters, (array)$values['parameters'])); } // Push middleware if multiple - if (isset($values['middleware'])) { + if (isset($values['middleware']) === true) { $this->setMiddlewares(array_merge((array)$values['middleware'], $this->middlewares)); } - if (isset($values['defaultParameterRegex'])) { + if (isset($values['defaultParameterRegex']) === true) { $this->setDefaultParameterRegex($values['defaultParameterRegex']); } @@ -463,7 +463,7 @@ abstract class Route implements IRoute /* Sort the parameters after the user-defined param order, if any */ $parameters = []; - if (count($this->originalParameters) > 0) { + if (count($this->originalParameters) !== 0) { $parameters = $this->originalParameters; } @@ -482,7 +482,7 @@ abstract class Route implements IRoute * If this is the first time setting parameters we store them so we * later can organize the array, in case somebody tried to sort the array. */ - if (count($parameters) > 0 && count($this->originalParameters) === 0) { + if (count($parameters) !== 0 && count($this->originalParameters) === 0) { $this->originalParameters = $parameters; } diff --git a/src/Pecee/SimpleRouter/Route/RouteController.php b/src/Pecee/SimpleRouter/Route/RouteController.php index bafa324..3489220 100644 --- a/src/Pecee/SimpleRouter/Route/RouteController.php +++ b/src/Pecee/SimpleRouter/Route/RouteController.php @@ -76,7 +76,7 @@ class RouteController extends LoadableRoute implements IControllerRoute $group = $this->getGroup(); - if ($group !== null && count($group->getDomains()) > 0) { + if ($group !== null && count($group->getDomains()) !== 0) { $url .= '//' . $group->getDomains()[0]; } @@ -97,7 +97,7 @@ class RouteController extends LoadableRoute implements IControllerRoute $strippedUrl = trim(str_ireplace($this->url, '/', $url), '/'); $path = explode('/', $strippedUrl); - if (count($path) > 0) { + if (count($path) !== 0) { $method = (isset($path[0]) === false || trim($path[0]) === '') ? $this->defaultMethod : $path[0]; $this->method = $request->getMethod() . ucfirst($method); diff --git a/src/Pecee/SimpleRouter/Route/RouteGroup.php b/src/Pecee/SimpleRouter/Route/RouteGroup.php index b1e687b..8a44003 100644 --- a/src/Pecee/SimpleRouter/Route/RouteGroup.php +++ b/src/Pecee/SimpleRouter/Route/RouteGroup.php @@ -28,7 +28,7 @@ class RouteGroup extends Route implements IGroupRoute $parameters = $this->parseParameters($domain, $request->getHost(), '.*'); - if ($parameters !== null && count($parameters) > 0) { + if ($parameters !== null && count($parameters) !== 0) { $this->parameters = $parameters; @@ -146,19 +146,19 @@ class RouteGroup extends Route implements IGroupRoute public function setSettings(array $values, $merge = false) { - if (isset($values['prefix'])) { + if (isset($values['prefix']) === true) { $this->setPrefix($values['prefix'] . $this->prefix); } - if ($merge === false && isset($values['exceptionHandler'])) { + if ($merge === false && isset($values['exceptionHandler']) === true) { $this->setExceptionHandlers((array)$values['exceptionHandler']); } - if ($merge === false && isset($values['domain'])) { + if ($merge === false && isset($values['domain']) === true) { $this->setDomains((array)$values['domain']); } - if (isset($values['as'])) { + if (isset($values['as']) === true) { if ($this->name !== null && $merge !== false) { $this->name = $values['as'] . '.' . $this->name; } else { @@ -188,7 +188,7 @@ class RouteGroup extends Route implements IGroupRoute $values['as'] = $this->name; } - if (count($this->parameters) > 0) { + if (count($this->parameters) !== 0) { $values['parameters'] = $this->parameters; } diff --git a/src/Pecee/SimpleRouter/Router.php b/src/Pecee/SimpleRouter/Router.php index d339952..a1eeff1 100644 --- a/src/Pecee/SimpleRouter/Router.php +++ b/src/Pecee/SimpleRouter/Router.php @@ -154,7 +154,7 @@ class Router if ($route->matchRoute($url, $this->request) === true) { /* Add exception handlers */ - if (count($route->getExceptionHandlers()) > 0) { + if (count($route->getExceptionHandlers()) !== 0) { /** @noinspection AdditionOperationOnArraysInspection */ $exceptionHandlers += $route->getExceptionHandlers(); } @@ -181,7 +181,7 @@ class Router $this->processedRoutes[] = $route; } - if (count($this->routeStack) > 0) { + if (count($this->routeStack) !== 0) { /* Pop and grab the routes added when executing group callback earlier */ $stack = $this->routeStack; @@ -203,7 +203,7 @@ class Router public function loadRoutes() { /* Initialize boot-managers */ - if (count($this->bootManagers) > 0) { + if (count($this->bootManagers) !== 0) { $max = count($this->bootManagers) - 1; @@ -247,7 +247,7 @@ class Router if ($route->matchRoute($url, $this->request) === true) { /* Check if request method matches */ - if (count($route->getRequestMethods()) > 0 && in_array($this->request->getMethod(), $route->getRequestMethods(), false) === false) { + if (count($route->getRequestMethods()) !== 0 && in_array($this->request->getMethod(), $route->getRequestMethods(), false) === false) { $routeNotAllowed = true; continue; } @@ -363,7 +363,7 @@ class Router public function arrayToParams(array $getParams = [], $includeEmpty = true) { - if (count($getParams) > 0) { + if (count($getParams) !== 0) { if ($includeEmpty === false) { $getParams = array_filter($getParams, function ($item) {