Merge pull request #532 from skipperbent/v4-development

Version 4.3.2.0
This commit is contained in:
Simon Sessingø
2021-04-01 02:37:52 +02:00
committed by GitHub
15 changed files with 414 additions and 42 deletions
+102 -30
View File
@@ -48,6 +48,7 @@ You can donate any amount of your choice by [clicking here](https://www.paypal.c
- [Namespaces](#namespaces) - [Namespaces](#namespaces)
- [Subdomain-routing](#subdomain-routing) - [Subdomain-routing](#subdomain-routing)
- [Route prefixes](#route-prefixes) - [Route prefixes](#route-prefixes)
- [Partial groups](#partial-groups)
- [Form Method Spoofing](#form-method-spoofing) - [Form Method Spoofing](#form-method-spoofing)
- [Accessing The Current Route](#accessing-the-current-route) - [Accessing The Current Route](#accessing-the-current-route)
- [Other examples](#other-examples) - [Other examples](#other-examples)
@@ -82,6 +83,8 @@ You can donate any amount of your choice by [clicking here](https://www.paypal.c
- [Custom EventHandlers](#custom-eventhandlers) - [Custom EventHandlers](#custom-eventhandlers)
- [Advanced](#advanced) - [Advanced](#advanced)
- [Disable multiple route rendering](#disable-multiple-route-rendering) - [Disable multiple route rendering](#disable-multiple-route-rendering)
- [Restrict access to IP](#restrict-access-to-ip)
- [Setting custom base path](#setting-custom-base-path)
- [Url rewriting](#url-rewriting) - [Url rewriting](#url-rewriting)
- [Changing current route](#changing-current-route) - [Changing current route](#changing-current-route)
- [Bootmanager: loading routes dynamically](#bootmanager-loading-routes-dynamically) - [Bootmanager: loading routes dynamically](#bootmanager-loading-routes-dynamically)
@@ -160,6 +163,8 @@ You can find the demo-project here: [https://github.com/skipperbent/simple-route
- Sub-domain routing - Sub-domain routing
- Custom boot managers to rewrite urls to "nicer" ones. - Custom boot managers to rewrite urls to "nicer" ones.
- Input manager; easily manage `GET`, `POST` and `FILE` values. - Input manager; easily manage `GET`, `POST` and `FILE` values.
- IP based restrictions.
- Easily extendable.
## Installation ## Installation
@@ -466,7 +471,8 @@ SimpleRouter::get('/posts/{post}/comments/{comment}', function ($postId, $commen
}); });
``` ```
**Note:** Route parameters are always encased within {} braces and should consist of alphabetic characters. Route parameters may not contain a - character. Use an underscore (_) instead. **Note:** Route parameters are always encased within `{` `}` braces and should consist of alphabetic characters. Route parameters can only contain certain characters like `A-Z`, `a-z`, `0-9`, `-` and `_`.
If your route contain other characters, please see [Custom regex for matching parameters](#custom-regex-for-matching-parameters).
### Optional parameters ### Optional parameters
@@ -681,6 +687,27 @@ SimpleRouter::group(['prefix' => '/lang/{language}'], function ($language) {
}); });
``` ```
## Partial groups
Partial router groups has the same benefits as a normal group, but **are only rendered once the url has matched**
in contrast to a normal group which are always rendered in order to retrieve it's child routes.
Partial groups are therefore more like a hybrid of a traditional route with the benefits of a group.
This can be extremely useful in situations where you only want special routes to be added, but only when a certain criteria or logic has been met.
**NOTE:** Use partial groups with caution as routes added within are only rendered and available once the url of the partial-group has matched.
This can cause `url()` not to find urls for the routes added within before the partial-group has been matched and is rendered.
**Example:**
```php
SimpleRouter::partialGroup('/plugin/{name}', function ($plugin) {
// Add routes from plugin
});
```
## Form Method Spoofing ## Form Method Spoofing
HTML forms do not support `PUT`, `PATCH` or `DELETE` actions. So, when defining `PUT`, `PATCH` or `DELETE` routes that are called from an HTML form, you will need to add a hidden `_method` field to the form. The value sent with the `_method` field will be used as the HTTP request method: HTML forms do not support `PUT`, `PATCH` or `DELETE` actions. So, when defining `PUT`, `PATCH` or `DELETE` routes that are called from an HTML form, you will need to add a hidden `_method` field to the form. The value sent with the `_method` field will be used as the HTTP request method:
@@ -1242,7 +1269,7 @@ All event callbacks will retrieve a `EventArgument` object as parameter. This ob
| `EVENT_ALL` | - | Fires when a event is triggered. | | `EVENT_ALL` | - | Fires when a event is triggered. |
| `EVENT_INIT` | - | Fires when router is initializing and before routes are loaded. | | `EVENT_INIT` | - | Fires when router is initializing and before routes are loaded. |
| `EVENT_LOAD` | `loadedRoutes` | Fires when all routes has been loaded and rendered, just before the output is returned. | | `EVENT_LOAD` | `loadedRoutes` | Fires when all routes has been loaded and rendered, just before the output is returned. |
| `EVENT_ADD_ROUTE` | `route` | Fires when route is added to the router. | | `EVENT_ADD_ROUTE` | `route`<br>`isSubRoute` | Fires when route is added to the router. `isSubRoute` is true when sub-route is rendered. |
| `EVENT_REWRITE` | `rewriteUrl`<br>`rewriteRoute` | Fires when a url-rewrite is and just before the routes are re-initialized. | | `EVENT_REWRITE` | `rewriteUrl`<br>`rewriteRoute` | Fires when a url-rewrite is and just before the routes are re-initialized. |
| `EVENT_BOOT` | `bootmanagers` | Fires when the router is booting. This happens just before boot-managers are rendered and before any routes has been loaded. | | `EVENT_BOOT` | `bootmanagers` | Fires when the router is booting. This happens just before boot-managers are rendered and before any routes has been loaded. |
| `EVENT_RENDER_BOOTMANAGER` | `bootmanagers`<br>`bootmanager` | Fires before a boot-manager is rendered. | | `EVENT_RENDER_BOOTMANAGER` | `bootmanagers`<br>`bootmanager` | Fires before a boot-manager is rendered. |
@@ -1370,6 +1397,74 @@ By default the router will try to execute all routes that matches a given url. T
This behavior can be easily disabled by setting `SimpleRouter::enableMultiRouteRendering(false)` in your `routes.php` file. This is the same behavior as version 3 and below. This behavior can be easily disabled by setting `SimpleRouter::enableMultiRouteRendering(false)` in your `routes.php` file. This is the same behavior as version 3 and below.
## Restrict access to IP
You can white and/or blacklist access to IP's using the build in `IpRestrictAccess` middleware.
Create your own custom Middleware and extend the `IpRestrictAccess` class.
The `IpRestrictAccess` class contains two properties `ipBlacklist` and `ipWhitelist` that can be added to your middleware to change which IP's that have access to your routes.
You can use `*` to restrict access to a range of ips.
```php
use \Pecee\Http\Middleware\IpRestrictAccess;
class IpBlockerMiddleware extends IpRestrictAccess
{
protected $ipBlacklist = [
'5.5.5.5',
'8.8.*',
];
protected $ipWhitelist = [
'8.8.2.2',
];
}
```
You can add the middleware to multiple routes by adding your [middleware to a group](#middleware).
## Setting custom base path
Sometimes it can be useful to add a custom base path to all of the routes added.
This can easily be done by taking advantage of the [Event Handlers](#events) support of the project.
```php
$basePath = '/basepath';
$eventHandler = new EventHandler();
$eventHandler->register(EventHandler::EVENT_ADD_ROUTE, function(EventArgument $event) use($basePath) {
$route = $event->route;
// Skip routes added by group as these will inherit the url
if(!$event->isSubRoute) {
return;
}
switch (true) {
case $route instanceof ILoadableRoute:
$route->prependUrl($basePath);
break;
case $route instanceof IGroupRoute:
$route->prependPrefix($basePath);
break;
}
});
TestRouter::addEventHandler($eventHandler);
```
In the example shown above, we create a new `EVENT_ADD_ROUTE` event that triggers, when a new route is added.
We skip all subroutes as these will inherit the url from their parent. Then, if the route is a group, we change the prefix
otherwise we change the url.
## Url rewriting ## Url rewriting
### Changing current route ### Changing current route
@@ -1656,40 +1751,17 @@ You can read more about adding your own custom regular expression for matching p
### Multiple routes matches? Which one has the priority? ### Multiple routes matches? Which one has the priority?
The router will match routes in the order they're added. The router will match routes in the order they're added and will render multiple routes, if they match.
It's possible to render multiple routes. If you want the router to stop when a route is matched, you simply return a value in your callback or stop the execution manually (using `response()->json()` etc.) or simply by returning a result.
If you want the router to stop when a route is matched, you simply return a value in your callback or stop the execution manually (using `response()->json()` etc.).
Any returned objects that implements the `__toString()` magic method will also prevent other routes from being rendered. Any returned objects that implements the `__toString()` magic method will also prevent other routes from being rendered.
If you want the router only to execute one route per request, you can [disabling multiple route rendering](#disable-multiple-route-rendering).
### Using the router on sub-paths ### Using the router on sub-paths
Using the library on a sub-path like `localhost/project/` is not officially supported, however it is possible to get it working quite easily. Please refer to [Setting custom base path](#setting-custom-base-path) part of the documentation.
Add an event that appends your sub-path when a new loadable route is added.
**Example:**
```php
// ... your routes.php file
if($isRunningLocally) {
$eventHandler = new EventHandler();
$eventHandler->register(EventHandler::EVENT_ADD_ROUTE, function (EventArgument $arg) use (&$status) {
if ($arg->route instanceof \Pecee\SimpleRouter\Route\LoadableRoute) {
$arg->route->prependUrl('/local-path');
}
});
TestRouter::addEventHandler($eventHandler);
}
```
## Debugging ## Debugging
+29 -5
View File
@@ -12,7 +12,17 @@ class BaseCsrfVerifier implements IMiddleware
public const POST_KEY = 'csrf_token'; public const POST_KEY = 'csrf_token';
public const HEADER_KEY = 'X-CSRF-TOKEN'; public const HEADER_KEY = 'X-CSRF-TOKEN';
/**
* Urls to ignore. You can use * to exclude all sub-urls on a given path.
* For example: /admin/*
* @var array|null
*/
protected $except; protected $except;
/**
* Urls to include. Can be used to include urls from a certain path.
* @var array|null
*/
protected $include;
protected $tokenProvider; protected $tokenProvider;
/** /**
@@ -34,11 +44,7 @@ class BaseCsrfVerifier implements IMiddleware
return false; return false;
} }
$max = count($this->except) - 1; foreach($this->except as $url) {
for ($i = $max; $i >= 0; $i--) {
$url = $this->except[$i];
$url = rtrim($url, '/'); $url = rtrim($url, '/');
if ($url[strlen($url) - 1] === '*') { if ($url[strlen($url) - 1] === '*') {
$url = rtrim($url, '*'); $url = rtrim($url, '*');
@@ -48,6 +54,24 @@ class BaseCsrfVerifier implements IMiddleware
} }
if ($skip === true) { if ($skip === true) {
if($this->include !== null && count($this->include) > 0) {
foreach($this->include as $includeUrl) {
$includeUrl = rtrim($includeUrl, '/');
if ($includeUrl[strlen($includeUrl) - 1] === '*') {
$includeUrl = rtrim($includeUrl, '*');
$skip = !$request->getUrl()->contains($includeUrl);
break;
}
$skip = !($includeUrl === $request->getUrl()->getOriginalUrl());
}
}
if($skip === false) {
continue;
}
return true; return true;
} }
} }
@@ -0,0 +1,43 @@
<?php
namespace Pecee\Http\Middleware;
use Pecee\Http\Request;
use Pecee\SimpleRouter\Exceptions\HttpException;
abstract class IpRestrictAccess implements IMiddleware
{
protected $ipBlacklist = [];
protected $ipWhitelist = [];
protected function validate(string $ip): bool
{
// Accept ip that is in white-list
if(in_array($ip, $this->ipWhitelist, true) === true) {
return true;
}
foreach ($this->ipBlacklist as $blackIp) {
// Blocks range (8.8.*)
if ($blackIp[strlen($blackIp) - 1] === '*' && strpos($ip, trim($blackIp, '*')) === 0) {
return false;
}
// Blocks exact match
if ($blackIp === $ip) {
return false;
}
}
return true;
}
public function handle(Request $request): void
{
if($this->validate((string)$request->getIp()) === false) {
throw new HttpException(sprintf('Restricted ip. Access to %s has been blocked', $request->getIp()), 403);
}
}
}
+4
View File
@@ -391,6 +391,10 @@ class Request
if ($this->url->getHost() === null) { if ($this->url->getHost() === null) {
$this->url->setHost((string)$this->getHost()); $this->url->setHost((string)$this->getHost());
} }
if($this->isSecure() === true) {
$this->url->setScheme('https');
}
} }
/** /**
@@ -53,6 +53,14 @@ interface IGroupRoute extends IRoute
*/ */
public function setDomains(array $domains): self; public function setDomains(array $domains): self;
/**
* Prepends prefix while ensuring that the url has the correct formatting.
*
* @param string $url
* @return static
*/
public function prependPrefix(string $url): self;
/** /**
* Set prefix that child-routes will inherit. * Set prefix that child-routes will inherit.
* *
@@ -40,7 +40,7 @@ interface ILoadableRoute extends IRoute
public function setUrl(string $url): self; public function setUrl(string $url): self;
/** /**
* Prepend url * Prepends url while ensuring that the url has the correct formatting.
* @param string $url * @param string $url
* @return ILoadableRoute * @return ILoadableRoute
*/ */
@@ -86,7 +86,7 @@ abstract class LoadableRoute extends Route implements ILoadableRoute
} }
/** /**
* Prepend url * Prepends url while ensuring that the url has the correct formatting.
* *
* @param string $url * @param string $url
* @return ILoadableRoute * @return ILoadableRoute
@@ -153,6 +153,17 @@ class RouteGroup extends Route implements IGroupRoute
return $this; return $this;
} }
/**
* Prepends prefix while ensuring that the url has the correct formatting.
*
* @param string $url
* @return static
*/
public function prependPrefix(string $url): IGroupRoute
{
return $this->setPrefix(rtrim($url, '/') . $this->prefix);
}
/** /**
* Set prefix that child-routes will inherit. * Set prefix that child-routes will inherit.
* *
@@ -1,8 +1,4 @@
<?php <?php
/**
* @deprecated This class is deprecated and will be removed in future versions.
* @see \Pecee\SimpleRouter\Route\RouteGroup
*/
namespace Pecee\SimpleRouter\Route; namespace Pecee\SimpleRouter\Route;
class RoutePartialGroup extends RouteGroup implements IPartialGroupRoute class RoutePartialGroup extends RouteGroup implements IPartialGroupRoute
+1 -1
View File
@@ -161,6 +161,7 @@ class Router
{ {
$this->fireEvents(EventHandler::EVENT_ADD_ROUTE, [ $this->fireEvents(EventHandler::EVENT_ADD_ROUTE, [
'route' => $route, 'route' => $route,
'isSubRoute' => $this->isProcessingRoute,
]); ]);
/* /*
@@ -184,7 +185,6 @@ class Router
*/ */
protected function renderAndProcess(IRoute $route): void protected function renderAndProcess(IRoute $route): void
{ {
$this->isProcessingRoute = true; $this->isProcessingRoute = true;
$route->renderRoute($this->request, $this); $route->renderRoute($this->request, $this);
$this->isProcessingRoute = false; $this->isProcessingRoute = false;
@@ -0,0 +1,66 @@
<?php
require_once 'Dummy/CsrfVerifier/DummyCsrfVerifier.php';
require_once 'Dummy/Security/SilentTokenProvider.php';
class CsrfVerifierTest extends \PHPUnit\Framework\TestCase
{
public function testTokenPass()
{
global $_POST;
$tokenProvider = new SilentTokenProvider();
$_POST[DummyCsrfVerifier::POST_KEY] = $tokenProvider->getToken();
TestRouter::router()->reset();
$router = TestRouter::router();
$router->getRequest()->setMethod(\Pecee\Http\Request::REQUEST_TYPE_POST);
$router->getRequest()->setUrl(new \Pecee\Http\Url('/page'));
$csrf = new DummyCsrfVerifier();
$csrf->setTokenProvider($tokenProvider);
$csrf->handle($router->getRequest());
// If handle doesn't throw exception, the test has passed
$this->assertTrue(true);
}
public function testTokenFail()
{
$this->expectException(\Pecee\Http\Middleware\Exceptions\TokenMismatchException::class);
global $_POST;
$tokenProvider = new SilentTokenProvider();
$router = TestRouter::router();
$router->getRequest()->setMethod(\Pecee\Http\Request::REQUEST_TYPE_POST);
$router->getRequest()->setUrl(new \Pecee\Http\Url('/page'));
$csrf = new DummyCsrfVerifier();
$csrf->setTokenProvider($tokenProvider);
$csrf->handle($router->getRequest());
}
public function testExcludeInclude()
{
$router = TestRouter::router();
$csrf = new DummyCsrfVerifier();
$request = $router->getRequest();
$request->setUrl(new \Pecee\Http\Url('/exclude-page'));
$this->assertTrue($csrf->testSkip($router->getRequest()));
$request->setUrl(new \Pecee\Http\Url('/exclude-all/page'));
$this->assertTrue($csrf->testSkip($router->getRequest()));
$request->setUrl(new \Pecee\Http\Url('/exclude-all/include-page'));
$this->assertFalse($csrf->testSkip($router->getRequest()));
$request->setUrl(new \Pecee\Http\Url('/include-page'));
$this->assertFalse($csrf->testSkip($router->getRequest()));
}
}
@@ -0,0 +1,71 @@
<?php
require_once 'Dummy/DummyController.php';
require_once 'Dummy/Middleware/IpRestrictMiddleware.php';
class CustomMiddlewareTest extends \PHPUnit\Framework\TestCase
{
public function testIpBlock() {
$this->expectException(\Pecee\SimpleRouter\Exceptions\HttpException::class);
global $_SERVER;
// Test exact ip
$_SERVER['remote-addr'] = '5.5.5.5';
TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() {
TestRouter::get('/fail', 'DummyController@method1');
});
TestRouter::debug('/fail');
// Test ip-range
$_SERVER['remote-addr'] = '8.8.4.4';
TestRouter::router()->reset();
TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() {
TestRouter::get('/fail', 'DummyController@method1');
});
TestRouter::debug('/fail');
}
public function testIpSuccess() {
global $_SERVER;
// Test ip that is not blocked
$_SERVER['remote-addr'] = '6.6.6.6';
TestRouter::router()->reset();
TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() {
TestRouter::get('/success', 'DummyController@method1');
});
TestRouter::debug('/success');
// Test ip in whitelist
$_SERVER['remote-addr'] = '8.8.2.2';
TestRouter::router()->reset();
TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() {
TestRouter::get('/success', 'DummyController@method1');
});
TestRouter::debug('/success');
$this->assertTrue(true);
}
}
@@ -0,0 +1,18 @@
<?php
class DummyCsrfVerifier extends \Pecee\Http\Middleware\BaseCsrfVerifier {
protected $except = [
'/exclude-page',
'/exclude-all/*',
];
protected $include = [
'/exclude-all/include-page',
];
public function testSkip(\Pecee\Http\Request $request) {
return $this->skip($request);
}
}
@@ -0,0 +1,14 @@
<?php
class IpRestrictMiddleware extends \Pecee\Http\Middleware\IpRestrictAccess {
protected $ipBlacklist = [
'5.5.5.5',
'8.8.*',
];
protected $ipWhitelist = [
'8.8.2.2',
];
}
@@ -103,4 +103,49 @@ class EventHandlerTest extends \PHPUnit\Framework\TestCase
} }
public function testCustomBasePath() {
$basePath = '/basepath/';
$eventHandler = new EventHandler();
$eventHandler->register(EventHandler::EVENT_ADD_ROUTE, function(EventArgument $data) use($basePath) {
// Skip routes added by group
if($data->isSubRoute === false) {
switch (true) {
case $data->route instanceof \Pecee\SimpleRouter\Route\ILoadableRoute:
$data->route->prependUrl($basePath);
break;
case $data->route instanceof \Pecee\SimpleRouter\Route\IGroupRoute:
$data->route->prependPrefix($basePath);
break;
}
}
});
$results = [];
TestRouter::addEventHandler($eventHandler);
TestRouter::get('/about', function() use(&$results) {
$results[] = 'about';
});
TestRouter::group(['prefix' => '/admin'], function() use(&$results) {
TestRouter::get('/', function() use(&$results) {
$results[] = 'admin';
});
});
TestRouter::router()->setRenderMultipleRoutes(false);
TestRouter::debugNoReset('/basepath/about');
TestRouter::debugNoReset('/basepath/admin');
$this->assertEquals(['about', 'admin'], $results);
}
} }