├── .gitignore
├── tests
├── bootstrap.php
├── Pecee
│ └── SimpleRouter
│ │ ├── Dummy
│ │ ├── Exception
│ │ │ ├── MiddlewareLoadedException.php
│ │ │ ├── ExceptionHandlerException.php
│ │ │ └── ResponseException.php
│ │ ├── NSController.php
│ │ ├── Handler
│ │ │ ├── ExceptionHandler.php
│ │ │ ├── ExceptionHandlerFirst.php
│ │ │ ├── ExceptionHandlerSecond.php
│ │ │ └── ExceptionHandlerThird.php
│ │ ├── Middleware
│ │ │ ├── IpRestrictMiddleware.php
│ │ │ ├── RewriteMiddleware.php
│ │ │ └── AuthMiddleware.php
│ │ ├── DummyMiddleware.php
│ │ ├── InputValidatorRules
│ │ │ └── ValidatorRuleCustomTest.php
│ │ ├── CsrfVerifier
│ │ │ └── DummyCsrfVerifier.php
│ │ ├── DummyLoadableController.php
│ │ ├── ClassLoader
│ │ │ └── CustomClassLoader.php
│ │ ├── ResourceController.php
│ │ ├── Managers
│ │ │ ├── FindUrlBootManager.php
│ │ │ └── TestBootManager.php
│ │ ├── Security
│ │ │ └── SilentTokenProvider.php
│ │ └── DummyController.php
│ │ ├── ClassLoaderTest.php
│ │ ├── MiddlewareTest.php
│ │ ├── BootManagerTest.php
│ │ ├── RouterCallbackExceptionHandlerTest.php
│ │ ├── RouterResourceTest.php
│ │ ├── CsrfVerifierTest.php
│ │ ├── RouterControllerTest.php
│ │ ├── RequestTest.php
│ │ ├── CustomMiddlewareTest.php
│ │ ├── RouterGroupTest.php
│ │ ├── EventHandlerTest.php
│ │ ├── InputParserTest.php
│ │ ├── RouterRewriteTest.php
│ │ ├── RouterRouteTest.php
│ │ └── InputHandlerTest.php
├── debug.php
└── TestRouter.php
├── src
└── Pecee
│ ├── SimpleRouter
│ ├── Route
│ │ ├── IPartialGroupRoute.php
│ │ ├── RoutePartialGroup.php
│ │ ├── IControllerRoute.php
│ │ ├── RouteUrl.php
│ │ ├── ILoadableRoute.php
│ │ ├── IGroupRoute.php
│ │ ├── RouteController.php
│ │ ├── IRoute.php
│ │ ├── RouteResource.php
│ │ ├── RouteGroup.php
│ │ └── LoadableRoute.php
│ ├── Exceptions
│ │ ├── NotFoundHttpException.php
│ │ ├── HttpException.php
│ │ └── ClassNotFoundHttpException.php
│ ├── Handlers
│ │ ├── IExceptionHandler.php
│ │ ├── IEventHandler.php
│ │ ├── CallbackExceptionHandler.php
│ │ ├── DebugEventHandler.php
│ │ └── EventHandler.php
│ ├── IRouterBootManager.php
│ ├── ClassLoader
│ │ ├── IClassLoader.php
│ │ └── ClassLoader.php
│ ├── Event
│ │ ├── IEventArgument.php
│ │ └── EventArgument.php
│ └── RouterUtils.php
│ ├── Exceptions
│ └── InvalidArgumentException.php
│ ├── Http
│ ├── Exceptions
│ │ └── MalformedUrlException.php
│ ├── Input
│ │ ├── Exceptions
│ │ │ ├── InputNotFoundException.php
│ │ │ ├── InputsNotValidatedException.php
│ │ │ └── InputValidationException.php
│ │ ├── IInputItem.php
│ │ ├── Attributes
│ │ │ ├── RouteGroup.php
│ │ │ ├── Route.php
│ │ │ └── ValidatorAttribute.php
│ │ ├── IInputHandler.php
│ │ ├── InputItem.php
│ │ ├── InputParser.php
│ │ ├── InputFile.php
│ │ └── InputValidator.php
│ ├── Security
│ │ ├── Exceptions
│ │ │ └── SecurityException.php
│ │ ├── ITokenProvider.php
│ │ └── CookieTokenProvider.php
│ ├── Middleware
│ │ ├── Exceptions
│ │ │ └── TokenMismatchException.php
│ │ ├── IMiddleware.php
│ │ ├── IpRestrictAccess.php
│ │ └── BaseCsrfVerifier.php
│ └── Response.php
│ └── Controllers
│ └── IResourceController.php
├── phpstan.neon
├── phpunit.xml
├── composer.json
└── .github
└── workflows
└── ci.yml
/.gitignore:
--------------------------------------------------------------------------------
1 | composer.lock
2 | vendor/
3 | .idea/
4 | .phpunit.result.cache
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | getMessage();
8 | }
9 |
10 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Route/RoutePartialGroup.php:
--------------------------------------------------------------------------------
1 | where(['name' => '[\w]+']);
7 | $debugInfo = SimpleRouter::startDebug();
8 | echo sprintf('
%s
', var_export($debugInfo, true));
9 | exit;
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/Dummy/Middleware/IpRestrictMiddleware.php:
--------------------------------------------------------------------------------
1 | setRewriteCallback(function() {
11 | return 'ok';
12 | });
13 |
14 | }
15 |
16 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandlerFirst.php:
--------------------------------------------------------------------------------
1 | setUrl(new \Pecee\Http\Url('/', false));
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandlerSecond.php:
--------------------------------------------------------------------------------
1 | setUrl(new \Pecee\Http\Url('/', false));
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/Dummy/Handler/ExceptionHandlerThird.php:
--------------------------------------------------------------------------------
1 | response = $response;
10 | parent::__construct('', 0);
11 | }
12 |
13 | public function getResponse(): string
14 | {
15 | return $this->response;
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/Dummy/Middleware/AuthMiddleware.php:
--------------------------------------------------------------------------------
1 | setRewriteCallback('DummyController@login');
13 | }
14 |
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/Dummy/CsrfVerifier/DummyCsrfVerifier.php:
--------------------------------------------------------------------------------
1 | skip($request);
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Route/IControllerRoute.php:
--------------------------------------------------------------------------------
1 | route;
31 | }
32 |
33 | /**
34 | * @return array|null
35 | */
36 | public function getSettings(): ?array{
37 | return $this->settings;
38 | }
39 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/Dummy/ClassLoader/CustomClassLoader.php:
--------------------------------------------------------------------------------
1 | result = &$result;
17 | }
18 |
19 | /**
20 | * Called when router loads it's routes
21 | *
22 | * @param \Pecee\SimpleRouter\Router $router
23 | * @param \Pecee\Http\Request $request
24 | */
25 | public function boot(\Pecee\SimpleRouter\Router $router, \Pecee\Http\Request $request): void
26 | {
27 | $contact = $router->findRoute('contact');
28 |
29 | if($contact !== null) {
30 | $this->result = true;
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/ClassLoader/IClassLoader.php:
--------------------------------------------------------------------------------
1 | rewrite = $rewrite;
11 | }
12 |
13 | /**
14 | * Called when router loads it's routes
15 | *
16 | * @param \Pecee\SimpleRouter\Router $router
17 | * @param \Pecee\Http\Request $request
18 | */
19 | public function boot(\Pecee\SimpleRouter\Router $router, \Pecee\Http\Request $request): void
20 | {
21 | foreach ($this->rewrite as $url => $rewrite) {
22 | // If the current url matches the rewrite url, we use our custom route
23 |
24 | if ($request->getUrl()->contains($url) === true) {
25 | $request->setRewriteUrl($rewrite);
26 | }
27 |
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 | src
18 |
19 |
20 |
21 |
22 | tests/Pecee/SimpleRouter/
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/ClassLoaderTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('method3', $classLoaderClass);
25 | $this->assertTrue($result);
26 |
27 | TestRouter::resetRouter();
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Event/IEventArgument.php:
--------------------------------------------------------------------------------
1 | refresh();
10 | }
11 |
12 | /**
13 | * Refresh existing token
14 | */
15 | public function refresh(): void
16 | {
17 | $this->token = uniqid('', false);
18 | }
19 |
20 | /**
21 | * Validate valid CSRF token
22 | *
23 | * @param string $token
24 | * @return bool
25 | */
26 | public function validate(string $token): bool
27 | {
28 | return ($token === $this->getToken());
29 | }
30 |
31 | /**
32 | * Get token token
33 | *
34 | * @param string|null $defaultValue
35 | * @return string|null
36 | */
37 | public function getToken(?string $defaultValue = null): ?string
38 | {
39 | return $this->token ?? $defaultValue;
40 | }
41 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/MiddlewareTest.php:
--------------------------------------------------------------------------------
1 | expectException(MiddlewareLoadedException::class);
12 |
13 | TestRouter::group(['exceptionHandler' => 'ExceptionHandler'], function () {
14 | TestRouter::get('/my/test/url', 'DummyController@method1', ['middleware' => 'DummyMiddleware']);
15 | });
16 |
17 | TestRouter::debug('/my/test/url', 'get');
18 |
19 | }
20 |
21 | public function testNestedMiddlewareDontLoad()
22 | {
23 |
24 | TestRouter::group(['exceptionHandler' => 'ExceptionHandler', 'middleware' => 'DummyMiddleware'], function () {
25 | TestRouter::get('/middleware', 'DummyController@method1');
26 | });
27 |
28 | TestRouter::get('/my/test/url', 'DummyController@method1');
29 |
30 | TestRouter::debug('/my/test/url', 'get');
31 |
32 | $this->assertTrue(true);
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Handlers/CallbackExceptionHandler.php:
--------------------------------------------------------------------------------
1 | callback = $callback;
31 | }
32 |
33 | /**
34 | * @param Request $request
35 | * @param Exception $error
36 | */
37 | public function handleError(Request $request, Exception $error): void
38 | {
39 | /* Fire exceptions */
40 | call_user_func($this->callback,
41 | $request,
42 | $error
43 | );
44 | }
45 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Exceptions/ClassNotFoundHttpException.php:
--------------------------------------------------------------------------------
1 | class = $class;
31 | $this->method = $method;
32 | }
33 |
34 | /**
35 | * Get class name
36 | * @return string
37 | */
38 | public function getClass(): string
39 | {
40 | return $this->class;
41 | }
42 |
43 | /**
44 | * Get method
45 | * @return string|null
46 | */
47 | public function getMethod(): ?string
48 | {
49 | return $this->method;
50 | }
51 |
52 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Route/RouteUrl.php:
--------------------------------------------------------------------------------
1 | setUrl($url);
18 | $this->setCallback($callback);
19 | }
20 |
21 | public function matchRoute(string $url, Request $request): bool
22 | {
23 | if ($this->getGroup() !== null && $this->getGroup()->matchRoute($url, $request) === false) {
24 | return false;
25 | }
26 |
27 | /* Match global regular-expression for route */
28 | $regexMatch = $this->matchRegex($request, $url);
29 |
30 | if ($regexMatch === false) {
31 | return false;
32 | }
33 |
34 | /* Parse parameters from current route */
35 | $parameters = $this->parseParameters($this->url, $url);
36 |
37 | /* If no custom regular expression or parameters was found on this route, we stop */
38 | if ($regexMatch === null && $parameters === null) {
39 | return false;
40 | }
41 |
42 | /* Set the parameters */
43 | $this->setParameters((array)$parameters);
44 |
45 | return true;
46 | }
47 |
48 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/ClassLoader/ClassLoader.php:
--------------------------------------------------------------------------------
1 | '/about',
25 | '/contact' => '/',
26 | ]));
27 |
28 | TestRouter::debug('/contact');
29 |
30 | $this->assertTrue($result);
31 | }
32 |
33 | public function testFindUrlFromBootManager()
34 | {
35 | TestRouter::get('/', 'DummyController@method1');
36 | TestRouter::get('/about', 'DummyController@method2')->name('about');
37 | TestRouter::get('/contact', 'DummyController@method3')->name('contact');
38 |
39 | $result = false;
40 |
41 | // Add boot-manager
42 | TestRouter::addBootManager(new FindUrlBootManager($result));
43 |
44 | TestRouter::debug('/');
45 |
46 | $this->assertTrue($result);
47 | }
48 |
49 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/Dummy/DummyController.php:
--------------------------------------------------------------------------------
1 | getRequest()->getInputHandler()->requireAttributeValues());
47 | }
48 |
49 | public function param($params = null)
50 | {
51 | echo join(', ', func_get_args());
52 | }
53 |
54 | public function getTest()
55 | {
56 | echo 'getTest';
57 | }
58 |
59 | public function postTest()
60 | {
61 | echo 'postTest';
62 | }
63 |
64 | public function putTest()
65 | {
66 | echo 'putTest';
67 | }
68 |
69 | public function login()
70 | {
71 | echo 'login';
72 | }
73 |
74 | }
--------------------------------------------------------------------------------
/src/Pecee/Http/Middleware/IpRestrictAccess.php:
--------------------------------------------------------------------------------
1 | ipWhitelist, true) === true) {
28 | return true;
29 | }
30 |
31 | foreach ($this->ipBlacklist as $blackIp) {
32 |
33 | // Blocks range (8.8.*)
34 | if ($blackIp[strlen($blackIp) - 1] === '*' && str_starts_with($ip, trim($blackIp, '*'))) {
35 | return false;
36 | }
37 |
38 | // Blocks exact match
39 | if ($blackIp === $ip) {
40 | return false;
41 | }
42 |
43 | }
44 |
45 | return true;
46 | }
47 |
48 | /**
49 | * @param Request $request
50 | * @throws HttpException
51 | */
52 | public function handle(Request $request): void
53 | {
54 | if($this->validate((string)$request->getIp()) === false) {
55 | throw new HttpException(sprintf('Restricted ip. Access to %s has been blocked', $request->getIp()), 403);
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Handlers/DebugEventHandler.php:
--------------------------------------------------------------------------------
1 | callback = static function (EventArgument $argument): void {
21 | // todo: log in database
22 | };
23 | }
24 |
25 | /**
26 | * Get events.
27 | *
28 | * @param string|null $name Filter events by name.
29 | * @return array
30 | */
31 | public function getEvents(?string $name): array
32 | {
33 | return [
34 | $name => [
35 | $this->callback,
36 | ],
37 | ];
38 | }
39 |
40 | /**
41 | * Fires any events registered with given event-name
42 | *
43 | * @param Router $router Router instance
44 | * @param string $name Event name
45 | * @param array $eventArgs Event arguments
46 | */
47 | public function fireEvents(Router $router, string $name, array $eventArgs = []): void
48 | {
49 | $callback = $this->callback;
50 | $callback(new EventArgument($name, $router, $eventArgs));
51 | }
52 |
53 | /**
54 | * Set debug callback
55 | *
56 | * @param Closure $event
57 | */
58 | public function setCallback(Closure $event): void
59 | {
60 | $this->callback = $event;
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/RouterCallbackExceptionHandlerTest.php:
--------------------------------------------------------------------------------
1 | expectException(ExceptionHandlerException::class);
13 |
14 | // Match normal route on alias
15 | TestRouter::get('/my-new-url', 'DummyController@method2');
16 | TestRouter::get('/my-url', 'DummyController@method1');
17 |
18 | TestRouter::error(function (\Pecee\Http\Request $request, \Exception $exception) {
19 | throw new ExceptionHandlerException();
20 | });
21 |
22 | TestRouter::debug('/404-url');
23 | }
24 |
25 | public function testExceptionHandlerCallback() {
26 |
27 | TestRouter::group(['prefix' => null], function() {
28 | TestRouter::get('/', function() {
29 | return 'Hello world';
30 | });
31 |
32 | TestRouter::get('/not-found', 'DummyController@method1');
33 | TestRouter::error(function(\Pecee\Http\Request $request, \Exception $exception) {
34 |
35 | if($exception instanceof \Pecee\SimpleRouter\Exceptions\NotFoundHttpException && $exception->getCode() === 404) {
36 | return $request->setRewriteCallback(static function() {
37 | return 'success';
38 | });
39 | }
40 | });
41 | });
42 |
43 | $result = TestRouter::debugOutput('/thisdoes-not/existssss', 'get');
44 | $this->assertEquals('success', $result);
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/tests/TestRouter.php:
--------------------------------------------------------------------------------
1 | setHost('testhost.com');
9 | }
10 |
11 | public static function debugNoReset(string $testUrl, string $testMethod = 'get'): void
12 | {
13 | $request = static::request();
14 |
15 | $request->setUrl((new \Pecee\Http\Url($testUrl, false))->setHost('local.unitTest'));
16 | $request->setMethod($testMethod);
17 |
18 | static::start();
19 | }
20 |
21 | public static function debug(string $testUrl, string $testMethod = 'get', bool $reset = true): void
22 | {
23 | try {
24 | static::debugNoReset($testUrl, $testMethod);
25 | } catch (\Exception $e) {
26 | static::$defaultNamespace = null;
27 | static::router()->reset();
28 | throw $e;
29 | }
30 |
31 | if ($reset === true) {
32 | static::$defaultNamespace = null;
33 | static::router()->reset();
34 | }
35 |
36 | }
37 |
38 | public static function debugOutput(string $testUrl, string $testMethod = 'get', bool $reset = true): string
39 | {
40 | $response = null;
41 |
42 | // Route request
43 | ob_start();
44 | static::debug($testUrl, $testMethod, $reset);
45 | $response = ob_get_clean();
46 |
47 | // Return response
48 | return $response;
49 | }
50 |
51 | public static function resetRouter(){
52 | global $_SERVER;
53 | unset($_SERVER['content_type']);
54 | unset($_SERVER['remote_addr']);
55 | unset($_SERVER['remote-addr']);
56 | unset($_SERVER['http-cf-connecting-ip']);
57 | unset($_SERVER['http-client-ip']);
58 | unset($_SERVER['http-x-forwarded-for']);
59 | unset($_SERVER['emote-addr']);
60 | global $_GET;
61 | $_GET = [];
62 | global $_POST;
63 | $_POST = [];
64 | TestRouter::router()->reset();
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/RouterResourceTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('store', $response);
14 | }
15 |
16 | public function testResourceCreate()
17 | {
18 | TestRouter::resource('/resource', 'ResourceController');
19 | $response = TestRouter::debugOutput('/resource/create', 'get');
20 |
21 | $this->assertEquals('create', $response);
22 |
23 | }
24 |
25 | public function testResourceIndex()
26 | {
27 | TestRouter::resource('/resource', 'ResourceController');
28 | $response = TestRouter::debugOutput('/resource', 'get');
29 |
30 | $this->assertEquals('index', $response);
31 | }
32 |
33 | public function testResourceDestroy()
34 | {
35 | TestRouter::resource('/resource', 'ResourceController');
36 | $response = TestRouter::debugOutput('/resource/38', 'delete');
37 |
38 | $this->assertEquals('destroy 38', $response);
39 | }
40 |
41 |
42 | public function testResourceEdit()
43 | {
44 | TestRouter::resource('/resource', 'ResourceController');
45 | $response = TestRouter::debugOutput('/resource/38/edit', 'get');
46 |
47 | $this->assertEquals('edit 38', $response);
48 |
49 | }
50 |
51 | public function testResourceUpdate()
52 | {
53 | TestRouter::resource('/resource', 'ResourceController');
54 | $response = TestRouter::debugOutput('/resource/38', 'put');
55 |
56 | $this->assertEquals('update 38', $response);
57 |
58 | }
59 |
60 | public function testResourceGet()
61 | {
62 | TestRouter::resource('/resource', 'ResourceController');
63 | $response = TestRouter::debugOutput('/resource/38', 'get');
64 |
65 | $this->assertEquals('show 38', $response);
66 |
67 | }
68 |
69 | }
--------------------------------------------------------------------------------
/src/Pecee/Http/Input/IInputHandler.php:
--------------------------------------------------------------------------------
1 |
65 | */
66 | public function all(array $filter = [], ...$methods): array;
67 |
68 | /**
69 | * @param array $filter
70 | * @param string|array ...$methods
71 | * @return array
72 | */
73 | public function values(array $filter = [], ...$methods): array;
74 | }
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build-test:
7 | runs-on: ${{ matrix.os }}
8 |
9 | env:
10 | PHP_EXTENSIONS: json
11 | PHP_INI_VALUES: assert.exception=1, zend.assertions=1
12 |
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | os:
17 | - ubuntu-latest
18 | - windows-latest
19 | php-version:
20 | - 8
21 | - 8.1
22 | - 8.2
23 | - 8.3
24 | - 8.4
25 | phpunit-version:
26 | - 9.5.4
27 | dependencies:
28 | - lowest
29 | - highest
30 | name: PHPUnit Tests
31 | steps:
32 | - name: Configure git to avoid issues with line endings
33 | if: matrix.os == 'windows-latest'
34 | run: git config --global core.autocrlf false
35 | - name: Checkout
36 | uses: actions/checkout@v4
37 | - name: Setup PHP
38 | uses: shivammathur/setup-php@v2
39 | with:
40 | php-version: ${{ matrix.php-version }}
41 | tools: composer:v5, phpunit:${{ matrix.phpunit-versions }}
42 | coverage: xdebug
43 | extensions: ${{ env.PHP_EXTENSIONS }}
44 | ini-values: ${{ env.PHP_INI_VALUES }}
45 | - name: Get composer cache directory
46 | id: composer-cache
47 | shell: bash
48 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
49 | - name: Cache dependencies
50 | uses: actions/cache@v4
51 | with:
52 | path: ${{ steps.composer-cache.outputs.dir }}
53 | key: php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-${{ hashFiles('**/composer.json') }}
54 | restore-keys: |
55 | php${{ matrix.php-version }}-composer-${{ matrix.dependencies }}-
56 | - name: Install lowest dependencies with composer
57 | if: matrix.dependencies == 'lowest'
58 | run: composer update --no-ansi --no-interaction --no-progress --prefer-lowest --ignore-platform-reqs
59 | - name: Install highest dependencies with composer
60 | if: matrix.dependencies == 'highest'
61 | run: composer update --no-ansi --no-interaction --no-progress --ignore-platform-reqs
62 | - name: Run tests with phpunit
63 | run: composer test
64 |
--------------------------------------------------------------------------------
/src/Pecee/Http/Input/Attributes/Route.php:
--------------------------------------------------------------------------------
1 | method;
49 | }
50 |
51 | /**
52 | * @return string
53 | */
54 | public function getRoute(): string
55 | {
56 | return $this->route;
57 | }
58 |
59 | /**
60 | * @return array|null
61 | */
62 | public function getSettings(): ?array
63 | {
64 | return $this->settings;
65 | }
66 |
67 | /**
68 | * @return string|null
69 | */
70 | public function getTitle(): ?string
71 | {
72 | return $this->title;
73 | }
74 |
75 | /**
76 | * @return string|null
77 | */
78 | public function getDescription(): ?string
79 | {
80 | return $this->description;
81 | }
82 |
83 | /**
84 | * @return string
85 | */
86 | public function getRequestContentType(): string
87 | {
88 | return $this->request_content_type;
89 | }
90 | }
--------------------------------------------------------------------------------
/src/Pecee/Http/Input/Exceptions/InputValidationException.php:
--------------------------------------------------------------------------------
1 | validation = $validation;
20 | parent::__construct($message, $code, $previous);
21 | }
22 |
23 | /**
24 | * @return Validation|null
25 | */
26 | public function getValidation(): ?Validation
27 | {
28 | return $this->validation;
29 | }
30 |
31 | /**
32 | * @return array
33 | */
34 | public function getErrorMessages(): array
35 | {
36 | if($this->getValidation() === null)
37 | return array();
38 | $errors = array();
39 | foreach ($this->getValidation()->errors()->toArray() as $key => $rule_errors){
40 | foreach ($rule_errors as $rule => $message){
41 | if(!isset($errors[$key]))
42 | $errors[$key] = array();
43 | $errors[$key][$rule] = (string) $message;
44 | }
45 | }
46 | return $errors;
47 | }
48 |
49 | /**
50 | * @param string $key
51 | * @return array|null
52 | */
53 | public function getErrorsForItem(string $key): ?array
54 | {
55 | if($this->getValidation() === null)
56 | return null;
57 | $errors = $this->getValidation()->errors()->get($key);
58 | if(empty($errors))
59 | return null;
60 | return $errors;
61 | }
62 |
63 | /**
64 | * @return string
65 | */
66 | public function getDetailedMessage(): string
67 | {
68 | $messages = array();
69 | foreach ($this->getErrorMessages() as $key => $rules){
70 | foreach ($rules as $rule => $message){
71 | $messages[] = $key . ': ' . $rule . ' - ' . $message;
72 | }
73 | }
74 | return 'Failed to validate inputs: ' . (sizeof($messages) === 0 ? 'keine' : join(';', $messages));
75 | }
76 |
77 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Route/ILoadableRoute.php:
--------------------------------------------------------------------------------
1 | getToken();
19 | $csrf = new DummyCsrfVerifier();
20 | $csrf->setTokenProvider($tokenProvider);
21 |
22 | $request = new Request(false);
23 | $request->setMethod(\Pecee\Http\Request::REQUEST_TYPE_POST);
24 | $request->setUrl(new \Pecee\Http\Url('/page', false));
25 | $request->fetch();
26 |
27 | $csrf->handle($request);
28 |
29 | // If handle doesn't throw exception, the test has passed
30 | $this->assertTrue(true);
31 | }
32 |
33 | public function testTokenFail()
34 | {
35 | $this->expectException(\Pecee\Http\Middleware\Exceptions\TokenMismatchException::class);
36 |
37 | global $_POST;
38 |
39 | $tokenProvider = new SilentTokenProvider();
40 | $csrf = new DummyCsrfVerifier();
41 | $csrf->setTokenProvider($tokenProvider);
42 |
43 | $request = new Request(false);
44 | $request->setMethod(\Pecee\Http\Request::REQUEST_TYPE_POST);
45 | $request->setUrl(new \Pecee\Http\Url('/page', false));
46 | $request->fetch();
47 |
48 | $csrf->handle($request);
49 | }
50 |
51 | public function testExcludeInclude()
52 | {
53 | $router = TestRouter::router();
54 | $csrf = new DummyCsrfVerifier();
55 | $request = $router->getRequest();
56 |
57 | $request->setUrl(new \Pecee\Http\Url('/exclude-page', false));
58 | $this->assertTrue($csrf->testSkip($router->getRequest()));
59 |
60 | $request->setUrl(new \Pecee\Http\Url('/exclude-all/page', false));
61 | $this->assertTrue($csrf->testSkip($router->getRequest()));
62 |
63 | $request->setUrl(new \Pecee\Http\Url('/exclude-all/include-page', false));
64 | $this->assertFalse($csrf->testSkip($router->getRequest()));
65 |
66 | $request->setUrl(new \Pecee\Http\Url('/include-page', false));
67 | $this->assertFalse($csrf->testSkip($router->getRequest()));
68 | }
69 |
70 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/RouterControllerTest.php:
--------------------------------------------------------------------------------
1 | assertEquals('getTest', $response);
17 |
18 | }
19 |
20 | public function testPost()
21 | {
22 | // Match normal route on alias
23 | TestRouter::controller('/url', 'DummyController');
24 |
25 | $response = TestRouter::debugOutput('/url/test', 'post');
26 |
27 | $this->assertEquals('postTest', $response);
28 |
29 | }
30 |
31 | public function testPut()
32 | {
33 | // Match normal route on alias
34 | TestRouter::controller('/url', 'DummyController');
35 |
36 | $response = TestRouter::debugOutput('/url/test', 'put');
37 |
38 | $this->assertEquals('putTest', $response);
39 |
40 | }
41 |
42 | public function testAutoload(){
43 | TestRouter::resetRouter();
44 |
45 | TestRouter::loadRoutes(\Dummy\DummyLoadableController::class);
46 |
47 | $response = TestRouter::debugOutput('/group/test2/endpoint');
48 | $this->assertEquals('method1', $response);
49 | }
50 |
51 | public function testAutoload2(){
52 | TestRouter::resetRouter();
53 | global $_POST;
54 |
55 | $_POST = [
56 | 'fullname' => 'Max Mustermann',
57 | 'company' => 'Company name'
58 | ];
59 |
60 | $request = new \Pecee\Http\Request(false);
61 | $request->setMethod('post');
62 | TestRouter::setRequest($request);
63 |
64 | TestRouter::loadRoutes(\Dummy\DummyLoadableController::class);
65 |
66 | $response = TestRouter::debugOutput('/group/test/url', 'post');
67 | $this->assertEquals('method2', $response);
68 | }
69 |
70 | public function testAutoloadWithoutGroup(){
71 | TestRouter::resetRouter();
72 | global $_POST;
73 |
74 | $_POST = [
75 | 'fullname' => 'Max Mustermann',
76 | 'company' => 'Company name'
77 | ];
78 |
79 | $request = new \Pecee\Http\Request(false);
80 | $request->setMethod('post');
81 | TestRouter::setRequest($request);
82 |
83 | TestRouter::loadRoutes(DummyController::class);
84 |
85 | $response = TestRouter::debugOutput('/my/test/url', 'post');
86 | $this->assertEquals('method4', $response);
87 | }
88 |
89 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Event/EventArgument.php:
--------------------------------------------------------------------------------
1 | eventName = $eventName;
35 | $this->router = $router;
36 | $this->arguments = $arguments;
37 | }
38 |
39 | /**
40 | * Get event name
41 | *
42 | * @return string
43 | */
44 | public function getEventName(): string
45 | {
46 | return $this->eventName;
47 | }
48 |
49 | /**
50 | * Set the event name
51 | *
52 | * @param string $name
53 | */
54 | public function setEventName(string $name): void
55 | {
56 | $this->eventName = $name;
57 | }
58 |
59 | /**
60 | * Get the router instance
61 | *
62 | * @return Router
63 | */
64 | public function getRouter(): Router
65 | {
66 | return $this->router;
67 | }
68 |
69 | /**
70 | * Get the request instance
71 | *
72 | * @return Request
73 | */
74 | public function getRequest(): Request
75 | {
76 | return $this->getRouter()->getRequest();
77 | }
78 |
79 | /**
80 | * @param string $name
81 | * @return mixed
82 | */
83 | public function __get(string $name)
84 | {
85 | return $this->arguments[$name] ?? null;
86 | }
87 |
88 | /**
89 | * @param string $name
90 | * @return bool
91 | */
92 | public function __isset(string $name): bool
93 | {
94 | return array_key_exists($name, $this->arguments);
95 | }
96 |
97 | /**
98 | * @param string $name
99 | * @param mixed $value
100 | * @throws InvalidArgumentException
101 | */
102 | public function __set(string $name, mixed $value): void
103 | {
104 | throw new InvalidArgumentException('Not supported');
105 | }
106 |
107 | /**
108 | * Get arguments
109 | *
110 | * @return array
111 | */
112 | public function getArguments(): array
113 | {
114 | return $this->arguments;
115 | }
116 |
117 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/RequestTest.php:
--------------------------------------------------------------------------------
1 | reset();
20 |
21 | $request = $router->getRequest();
22 |
23 | $callback($request);
24 |
25 | // Reset everything
26 | $_SERVER[$name] = null;
27 | $router->reset();
28 | }
29 |
30 | public function testContentTypeParse()
31 | {
32 | global $_SERVER;
33 |
34 | // Test normal content-type
35 |
36 | $contentType = 'application/x-www-form-urlencoded';
37 |
38 | $this->processHeader('content_type', $contentType, function(\Pecee\Http\Request $request) use($contentType) {
39 | $this->assertEquals($contentType, $request->getContentType());
40 | });
41 |
42 | // Test special content-type with encoding
43 |
44 | $contentTypeWithEncoding = 'application/x-www-form-urlencoded; charset=UTF-8';
45 |
46 | $this->processHeader('content_type', $contentTypeWithEncoding, function(\Pecee\Http\Request $request) use($contentType) {
47 | $this->assertEquals($contentType, $request->getContentType());
48 | });
49 | }
50 |
51 | public function testGetIp()
52 | {
53 | $ip = '1.1.1.1';
54 | $this->processHeader('remote_addr', $ip, function(\Pecee\Http\Request $request) use($ip) {
55 | $this->assertEquals($ip, $request->getIp());
56 | });
57 |
58 | $ip = '2.2.2.2';
59 | $this->processHeader('http-cf-connecting-ip', $ip, function(\Pecee\Http\Request $request) use($ip) {
60 | $this->assertEquals($ip, $request->getIp());
61 | });
62 |
63 | $ip = '3.3.3.3';
64 | $this->processHeader('http-client-ip', $ip, function(\Pecee\Http\Request $request) use($ip) {
65 | $this->assertEquals($ip, $request->getIp());
66 | });
67 |
68 | $ip = '4.4.4.4';
69 | $this->processHeader('http-x-forwarded-for', $ip, function(\Pecee\Http\Request $request) use($ip) {
70 | $this->assertEquals($ip, $request->getIp());
71 | });
72 |
73 | // Test safe
74 |
75 | $ip = '5.5.5.5';
76 | $this->processHeader('http-x-forwarded-for', $ip, function(\Pecee\Http\Request $request) {
77 | $this->assertEquals(null, $request->getIp(true));
78 | });
79 |
80 | }
81 |
82 | // TODO: implement more test-cases
83 |
84 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/RouterUtils.php:
--------------------------------------------------------------------------------
1 | request()->getInputHandler()->value($index, $defaultValue, ...$methods);
65 | }
66 |
67 | return $this->request()->getInputHandler();
68 | }
69 |
70 | /**
71 | * @param string $url
72 | * @param int|null $code
73 | */
74 | public function redirect(string $url, ?int $code = null): void
75 | {
76 | if($code !== null){
77 | $this->response()->httpCode($code);
78 | }
79 |
80 | $this->response()->redirect($url);
81 | }
82 |
83 | /**
84 | * Get current csrf-token
85 | * @return string|null
86 | */
87 | public function csrf_token(): ?string
88 | {
89 | $baseVerifier = Router::router()->getCsrfVerifier();
90 |
91 | return $baseVerifier?->getTokenProvider()->getToken();
92 | }
93 | }
--------------------------------------------------------------------------------
/src/Pecee/Http/Input/Attributes/ValidatorAttribute.php:
--------------------------------------------------------------------------------
1 | validator = $validator;
34 | $this->setType($type);
35 | }
36 |
37 | /**
38 | * @return string|null
39 | */
40 | public function getName(): ?string
41 | {
42 | return $this->name;
43 | }
44 |
45 | /**
46 | * @param string $name
47 | */
48 | public function setName(string $name): void
49 | {
50 | $this->name = $name;
51 | }
52 |
53 | /**
54 | * @return string|null
55 | */
56 | public function getType(): ?string
57 | {
58 | return $this->type;
59 | }
60 |
61 | /**
62 | * @return string|null
63 | */
64 | public function getValidatorType(): ?string
65 | {
66 | return match ($this->type){
67 | 'bool' => 'boolean',
68 | 'int' => 'integer',
69 | default => $this->type
70 | };
71 | }
72 |
73 | /**
74 | * @param string|null $type
75 | */
76 | public function setType(?string $type): void
77 | {
78 | if($type === null){
79 | $this->type = null;
80 | return;
81 | }
82 | if(str_starts_with($type, '?')){
83 | $type = substr($type, 1);
84 | array_unshift($this->validator, 'nullable');
85 | }
86 | $this->type = $type;
87 | }
88 |
89 | /**
90 | * @return array
91 | */
92 | public function getValidator(): array
93 | {
94 | return $this->validator;
95 | }
96 |
97 | /**
98 | * @param string $validator
99 | * @return void
100 | */
101 | public function addValidator(string $validator): void
102 | {
103 | if(!in_array($validator, $this->validator))
104 | $this->validator[] = $validator;
105 | }
106 |
107 | /**
108 | * @return array
109 | */
110 | public function getFullValidator(): array
111 | {
112 | $validator = $this->getValidator();
113 | if($this->getValidatorType() !== null && !in_array($this->getValidatorType(), $validator))
114 | array_unshift($validator, $this->getValidatorType());
115 | if(!in_array('nullable', $validator))
116 | array_unshift($validator, 'required');
117 | return $validator;
118 | }
119 |
120 | /**
121 | * @return mixed
122 | */
123 | public function getExample(): mixed
124 | {
125 | return $this->example;
126 | }
127 |
128 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/CustomMiddlewareTest.php:
--------------------------------------------------------------------------------
1 | '/','middleware' => AuthMiddleware::class], function () {
17 | TestRouter::get('/home', 'DummyController@method3');
18 | });
19 |
20 | TestRouter::get('/login', 'DummyController@login');
21 |
22 |
23 | $out = TestRouter::debugOutput('/home');
24 | $this->assertEquals('login', $out);
25 |
26 | TestRouter::resetRouter();
27 |
28 | AuthMiddleware::$auth = false;
29 | TestRouter::group(['prefix' => '/','middleware' => AuthMiddleware::class], function () {
30 | TestRouter::get('/home', 'DummyController@method3');
31 | });
32 |
33 | TestRouter::get('/login', 'DummyController@login');
34 | $out = TestRouter::debugOutput('/home');
35 | $this->assertEquals('method3', $out);
36 | }
37 |
38 | public function testIpBlock() {
39 |
40 | $this->expectException(\Pecee\SimpleRouter\Exceptions\HttpException::class);
41 |
42 | global $_SERVER;
43 |
44 | // Test exact ip
45 | TestRouter::resetRouter();
46 |
47 | $_SERVER['remote-addr'] = '5.5.5.5';
48 |
49 | $request = new Request(false);
50 | $request->setMethod('get');
51 | TestRouter::setRequest($request);
52 |
53 | TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() {
54 | TestRouter::get('/fail', 'DummyController@method1');
55 | });
56 |
57 | TestRouter::debug('/fail');
58 |
59 | // Test ip-range
60 |
61 |
62 | TestRouter::resetRouter();
63 |
64 | $_SERVER['remote-addr'] = '8.8.4.4';
65 |
66 | $request = new Request(false);
67 | $request->setMethod('get');
68 | TestRouter::setRequest($request);
69 |
70 | TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() {
71 | TestRouter::get('/fail', 'DummyController@method1');
72 | });
73 |
74 | TestRouter::debug('/fail');
75 |
76 | }
77 |
78 | public function testIpSuccess() {
79 |
80 | global $_SERVER;
81 |
82 | TestRouter::resetRouter();
83 | // Test ip that is not blocked
84 |
85 | $_SERVER['remote-addr'] = '6.6.6.6';
86 |
87 | TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() {
88 | TestRouter::get('/success', 'DummyController@method1');
89 | });
90 |
91 | TestRouter::debug('/success');
92 |
93 | // Test ip in whitelist
94 | TestRouter::resetRouter();
95 |
96 | $_SERVER['remote-addr'] = '8.8.2.2';
97 |
98 | TestRouter::group(['middleware' => IpRestrictMiddleware::class], function() {
99 | TestRouter::get('/success', 'DummyController@method1');
100 | });
101 |
102 | TestRouter::debug('/success');
103 |
104 | $this->assertTrue(true);
105 |
106 | }
107 |
108 | }
--------------------------------------------------------------------------------
/src/Pecee/Http/Security/CookieTokenProvider.php:
--------------------------------------------------------------------------------
1 | token = ($this->hasToken() === true) ? $_COOKIE[static::CSRF_KEY] : null;
29 |
30 | if ($this->token === null) {
31 | $this->token = $this->generateToken();
32 | }
33 | }
34 |
35 | /**
36 | * Generate random identifier for CSRF token
37 | *
38 | * @return string
39 | * @throws SecurityException
40 | */
41 | public function generateToken(): string
42 | {
43 | try {
44 | return bin2hex(random_bytes(32));
45 | } catch (Exception $e) {
46 | throw new SecurityException($e->getMessage(), (int)$e->getCode(), $e->getPrevious());
47 | }
48 | }
49 |
50 | /**
51 | * Validate valid CSRF token
52 | *
53 | * @param string $token
54 | * @return bool
55 | */
56 | public function validate(string $token): bool
57 | {
58 | if ($this->getToken() !== null) {
59 | return hash_equals($token, $this->getToken());
60 | }
61 |
62 | return false;
63 | }
64 |
65 | /**
66 | * Set csrf token cookie
67 | * Overwrite this method to save the token to another storage like session etc.
68 | *
69 | * @param string $token
70 | */
71 | public function setToken(string $token): void
72 | {
73 | $this->token = $token;
74 | setcookie(static::CSRF_KEY, $token, time() + (60 * $this->cookieTimeoutMinutes), '/', ini_get('session.cookie_domain'), ini_get('session.cookie_secure'), ini_get('session.cookie_httponly'));
75 | }
76 |
77 | /**
78 | * Get csrf token
79 | * @param string|null $defaultValue
80 | * @return string|null
81 | */
82 | public function getToken(?string $defaultValue = null): ?string
83 | {
84 | return $this->token ?? $defaultValue;
85 | }
86 |
87 | /**
88 | * Refresh existing token
89 | */
90 | public function refresh(): void
91 | {
92 | if ($this->token !== null) {
93 | $this->setToken($this->token);
94 | }
95 | }
96 |
97 | /**
98 | * Returns whether the csrf token has been defined
99 | * @return bool
100 | */
101 | public function hasToken(): bool
102 | {
103 | return isset($_COOKIE[static::CSRF_KEY]);
104 | }
105 |
106 | /**
107 | * Get timeout for cookie in minutes
108 | * @return int
109 | */
110 | public function getCookieTimeoutMinutes(): int
111 | {
112 | return $this->cookieTimeoutMinutes;
113 | }
114 |
115 | /**
116 | * Set cookie timeout in minutes
117 | * @param int $minutes
118 | */
119 | public function setCookieTimeoutMinutes(int $minutes): void
120 | {
121 | $this->cookieTimeoutMinutes = $minutes;
122 | }
123 |
124 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/RouterGroupTest.php:
--------------------------------------------------------------------------------
1 | '/group'], function () use (&$result) {
14 | $result = true;
15 | });
16 |
17 | try {
18 | TestRouter::debug('/', 'get');
19 | } catch (\Exception $e) {
20 |
21 | }
22 | $this->assertTrue($result);
23 | }
24 |
25 | public function testNestedGroup()
26 | {
27 |
28 | TestRouter::group(['prefix' => '/api'], function () {
29 |
30 | TestRouter::group(['prefix' => '/v1'], function () {
31 | TestRouter::get('/test', 'DummyController@method1');
32 | });
33 |
34 | });
35 |
36 | TestRouter::debug('/api/v1/test', 'get');
37 |
38 | $this->assertTrue(true);
39 | }
40 |
41 | public function testMultipleRoutes()
42 | {
43 |
44 | TestRouter::group(['prefix' => '/api'], function () {
45 |
46 | TestRouter::group(['prefix' => '/v1'], function () {
47 | TestRouter::get('/test', 'DummyController@method1');
48 | });
49 |
50 | });
51 |
52 | TestRouter::get('/my/match', 'DummyController@method1');
53 |
54 | TestRouter::group(['prefix' => '/service'], function () {
55 |
56 | TestRouter::group(['prefix' => '/v1'], function () {
57 | TestRouter::get('/no-match', 'DummyController@method1');
58 | });
59 |
60 | });
61 |
62 | TestRouter::debug('/my/match', 'get');
63 |
64 | $this->assertTrue(true);
65 | }
66 |
67 | public function testUrls()
68 | {
69 | // Test array name
70 | TestRouter::get('/my/fancy/url/1', 'DummyController@method1', ['as' => 'fancy1']);
71 |
72 | // Test method name
73 | TestRouter::get('/my/fancy/url/2', 'DummyController@method1')->setName('fancy2');
74 |
75 | TestRouter::debugNoReset('/my/fancy/url/1');
76 |
77 | $this->assertEquals('/my/fancy/url/1/', TestRouter::getUrl('fancy1'));
78 | $this->assertEquals('/my/fancy/url/2/', TestRouter::getUrl('fancy2'));
79 |
80 | TestRouter::resetRouter();
81 |
82 | }
83 |
84 | public function testNamespaceExtend()
85 | {
86 | TestRouter::group(['namespace' => '\My\Namespace'], function () use (&$result) {
87 |
88 | TestRouter::group(['namespace' => 'Service'], function () use (&$result) {
89 |
90 | TestRouter::get('/test', function () use (&$result) {
91 | return \Pecee\SimpleRouter\SimpleRouter::router()->getRequest()->getLoadedRoute()->getNamespace();
92 | });
93 |
94 | });
95 |
96 | });
97 |
98 | $namespace = TestRouter::debugOutput('/test');
99 | $this->assertEquals('\My\Namespace\Service', $namespace);
100 | }
101 |
102 | public function testNamespaceOverwrite()
103 | {
104 | TestRouter::group(['namespace' => '\My\Namespace'], function () use (&$result) {
105 |
106 | TestRouter::group(['namespace' => '\Service'], function () use (&$result) {
107 |
108 | TestRouter::get('/test', function () use (&$result) {
109 | return \Pecee\SimpleRouter\SimpleRouter::router()->getRequest()->getLoadedRoute()->getNamespace();
110 | });
111 |
112 | });
113 |
114 | });
115 |
116 | $namespace = TestRouter::debugOutput('/test');
117 | $this->assertEquals('\Service', $namespace);
118 | }
119 |
120 | }
--------------------------------------------------------------------------------
/src/Pecee/Http/Middleware/BaseCsrfVerifier.php:
--------------------------------------------------------------------------------
1 | tokenProvider = new CookieTokenProvider();
39 | }
40 |
41 | /**
42 | * Check if the url matches the urls in the except property
43 | * @param Request $request
44 | * @return bool
45 | */
46 | protected function skip(Request $request): bool
47 | {
48 | if ($this->except === null || count($this->except) === 0) {
49 | return false;
50 | }
51 |
52 | foreach($this->except as $url) {
53 | $url = rtrim($url, '/');
54 | if ($url[strlen($url) - 1] === '*') {
55 | $url = rtrim($url, '*');
56 | $skip = $request->getUrl()->contains($url);
57 | } else {
58 | $skip = ($url === rtrim($request->getUrl()->getRelativeUrl(false), '/'));
59 | }
60 |
61 | if ($skip === true) {
62 |
63 | if($this->include !== null && count($this->include) > 0) {
64 | foreach($this->include as $includeUrl) {
65 | $includeUrl = rtrim($includeUrl, '/');
66 | if ($includeUrl[strlen($includeUrl) - 1] === '*') {
67 | $includeUrl = rtrim($includeUrl, '*');
68 | $skip = !$request->getUrl()->contains($includeUrl);
69 | break;
70 | }
71 |
72 | $skip = !($includeUrl === rtrim($request->getUrl()->getRelativeUrl(false), '/'));
73 | }
74 | }
75 |
76 | if($skip === false) {
77 | continue;
78 | }
79 |
80 | return true;
81 | }
82 | }
83 |
84 | return false;
85 | }
86 |
87 | /**
88 | * Handle request
89 | *
90 | * @param Request $request
91 | * @throws TokenMismatchException
92 | */
93 | public function handle(Request $request): void
94 | {
95 | if ($this->skip($request) === false && $request->isPostBack() === true) {
96 |
97 | $token = $request->getInputHandler()->value(
98 | static::POST_KEY,
99 | $request->getHeader(static::HEADER_KEY),
100 | Request::$requestTypesPost
101 | );
102 |
103 | if ($this->tokenProvider->validate((string)$token) === false) {
104 | throw new TokenMismatchException('Invalid CSRF-token.');
105 | }
106 |
107 | }
108 |
109 | // Refresh existing token
110 | $this->tokenProvider->refresh();
111 | }
112 |
113 | /**
114 | * @return ITokenProvider
115 | */
116 | public function getTokenProvider(): ITokenProvider
117 | {
118 | return $this->tokenProvider;
119 | }
120 |
121 | /**
122 | * Set token provider
123 | * @param ITokenProvider $provider
124 | */
125 | public function setTokenProvider(ITokenProvider $provider): void
126 | {
127 | $this->tokenProvider = $provider;
128 | }
129 |
130 | }
--------------------------------------------------------------------------------
/src/Pecee/Http/Input/InputItem.php:
--------------------------------------------------------------------------------
1 | index = $index;
34 | $this->value = $value;
35 |
36 | // Make the name human friendly, by replace _ with space
37 | $this->name = ucfirst(str_replace('_', ' ', strtolower($this->index)));
38 | }
39 |
40 | /**
41 | * @return string
42 | */
43 | public function getIndex(): string
44 | {
45 | return $this->index;
46 | }
47 |
48 | /**
49 | * @param string $index
50 | * @return IInputItem
51 | */
52 | public function setIndex(string $index): IInputItem
53 | {
54 | $this->index = $index;
55 |
56 | return $this;
57 | }
58 |
59 | /**
60 | * @return string|null
61 | */
62 | public function getName(): ?string
63 | {
64 | return $this->name;
65 | }
66 |
67 | /**
68 | * Set input name
69 | * @param string $name
70 | * @return static
71 | */
72 | public function setName(string $name): IInputItem
73 | {
74 | $this->name = $name;
75 |
76 | return $this;
77 | }
78 |
79 | /**
80 | * @return mixed
81 | */
82 | public function getValue(): mixed
83 | {
84 | if(is_array($this->value)){
85 | return $this->parseValueFromArray($this->value);
86 | }
87 | return $this->value;
88 | }
89 |
90 | /**
91 | * @return bool
92 | */
93 | public function hasInputItems(): bool
94 | {
95 | return is_array($this->value) && (array_keys($this->value) !== range(0, count($this->value) - 1) || (sizeof($this->value) > 0 && is_array($this->value[0])));
96 | }
97 |
98 | /**
99 | * @return InputItem[]
100 | */
101 | public function getInputItems(): array
102 | {
103 | if($this->hasInputItems()){
104 | return $this->value;
105 | }
106 | return array();
107 | }
108 |
109 | /**
110 | * @param array $array
111 | * @return array
112 | */
113 | protected function parseValueFromArray(array $array): array
114 | {
115 | $output = [];
116 | /* @var $item InputItem */
117 | foreach ($array as $key => $item) {
118 |
119 | if ($item instanceof IInputItem) {
120 | $item = $item->getValue();
121 | }
122 |
123 | $output[$key] = is_array($item) ? $this->parseValueFromArray($item) : $item;
124 | }
125 |
126 | return $output;
127 | }
128 |
129 | /**
130 | * Set input value
131 | * @param mixed $value
132 | * @return static
133 | */
134 | public function setValue(mixed $value): IInputItem
135 | {
136 | $this->value = $value;
137 |
138 | return $this;
139 | }
140 |
141 | /**
142 | * @return InputParser
143 | */
144 | public function parser(): InputParser{
145 | return new InputParser($this);
146 | }
147 |
148 | /**
149 | * @param string|array $rules
150 | * @return Validation
151 | * @throws InputValidationException
152 | */
153 | public function validate(string|array $rules): Validation{
154 | return InputValidator::make()->validateItem($this, $rules);
155 | }
156 |
157 | //TODO integrate into php 8 update
158 | /*public function offsetExists($offset): bool
159 | {
160 | return isset($this->value[$offset]);
161 | }
162 |
163 | public function offsetGet($offset)
164 | {
165 | if ($this->offsetExists($offset) === true) {
166 | return $this->value[$offset];
167 | }
168 |
169 | return null;
170 | }
171 |
172 | public function offsetSet($offset, $value): void
173 | {
174 | $this->value[$offset] = $value;
175 | }
176 |
177 | public function offsetUnset($offset): void
178 | {
179 | unset($this->value[$offset]);
180 | }*/
181 |
182 | public function __toString(): string
183 | {
184 | return $this->getIndex() . ':' . json_encode($this->getValue());
185 | }
186 |
187 | public function getIterator(): ArrayIterator
188 | {
189 | return new ArrayIterator($this->getValue());
190 | }
191 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/EventHandlerTest.php:
--------------------------------------------------------------------------------
1 | reset();
18 | $events = EventHandler::$events;
19 |
20 | // Remove the all event
21 | unset($events[\array_search(EventHandler::EVENT_ALL, $events, true)]);
22 |
23 | $eventHandler = new EventHandler();
24 | $eventHandler->register(EventHandler::EVENT_ALL, function (EventArgument $arg) use (&$events) {
25 | $key = \array_search($arg->getEventName(), $events, true);
26 | unset($events[$key]);
27 | });
28 |
29 | TestRouter::addEventHandler($eventHandler);
30 |
31 | // Add rewrite
32 | TestRouter::error(function (\Pecee\Http\Request $request, \Exception $error) {
33 |
34 | // Trigger rewrite
35 | $request->setRewriteUrl('/');
36 |
37 | });
38 |
39 | TestRouter::get('/', 'DummyController@method1')->name('home');
40 |
41 | // Add csrf-verifier
42 | $csrfVerifier = new \Pecee\Http\Middleware\BaseCsrfVerifier();
43 | $csrfVerifier->setTokenProvider(new SilentTokenProvider());
44 | TestRouter::csrfVerifier($csrfVerifier);
45 |
46 | // Add boot-manager
47 | TestRouter::addBootManager(new TestBootManager([
48 | '/test' => '/',
49 | ]));
50 |
51 | // Start router
52 | TestRouter::debug('/non-existing', reset: false);
53 |
54 | // Trigger findRoute
55 | TestRouter::router()->findRoute('home');
56 |
57 | // Trigger getUrl
58 | TestRouter::router()->getUrl('home');
59 |
60 | $this->assertEquals($events, []);
61 | }
62 |
63 | public function testAllEvent()
64 | {
65 | $status = false;
66 |
67 | $eventHandler = new EventHandler();
68 | $eventHandler->register(EventHandler::EVENT_ALL, function (EventArgument $arg) use (&$status) {
69 | $status = true;
70 | });
71 |
72 | TestRouter::addEventHandler($eventHandler);
73 |
74 | TestRouter::get('/', 'DummyController@method1');
75 | TestRouter::debug('/');
76 |
77 | // All event should fire for each other event
78 | $this->assertEquals(true, $status);
79 | }
80 |
81 | public function testPrefixEvent()
82 | {
83 |
84 | $eventHandler = new EventHandler();
85 | $eventHandler->register(EventHandler::EVENT_ADD_ROUTE, function (EventArgument $arg) use (&$status) {
86 |
87 | if ($arg->route instanceof \Pecee\SimpleRouter\Route\LoadableRoute) {
88 | $arg->route->prependUrl('/local-path');
89 | }
90 |
91 | });
92 |
93 | TestRouter::addEventHandler($eventHandler);
94 |
95 | $status = false;
96 |
97 | TestRouter::get('/', function () use (&$status) {
98 | $status = true;
99 | });
100 |
101 | TestRouter::debug('/local-path');
102 |
103 | $this->assertTrue($status);
104 |
105 | }
106 |
107 | public function testCustomBasePath() {
108 |
109 | $basePath = '/basepath/';
110 |
111 | $eventHandler = new EventHandler();
112 | $eventHandler->register(EventHandler::EVENT_ADD_ROUTE, function(EventArgument $data) use($basePath) {
113 |
114 | // Skip routes added by group
115 | if($data->isSubRoute === false) {
116 |
117 | switch (true) {
118 | case $data->route instanceof \Pecee\SimpleRouter\Route\ILoadableRoute:
119 | $data->route->prependUrl($basePath);
120 | break;
121 | case $data->route instanceof \Pecee\SimpleRouter\Route\IGroupRoute:
122 | $data->route->prependPrefix($basePath);
123 | break;
124 |
125 | }
126 | }
127 |
128 | });
129 |
130 | $results = [];
131 |
132 | TestRouter::addEventHandler($eventHandler);
133 |
134 | TestRouter::get('/about', function() use(&$results) {
135 | $results[] = 'about';
136 | });
137 |
138 | TestRouter::group(['prefix' => '/admin'], function() use(&$results) {
139 | TestRouter::get('/', function() use(&$results) {
140 | $results[] = 'admin';
141 | });
142 | });
143 |
144 | TestRouter::router()->setRenderMultipleRoutes(false);
145 | TestRouter::debugNoReset('/basepath/about');
146 | TestRouter::debugNoReset('/basepath/admin');
147 |
148 | $this->assertEquals(['about', 'admin'], $results);
149 |
150 | }
151 |
152 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/InputParserTest.php:
--------------------------------------------------------------------------------
1 | 'Max Mustermann',
21 | 'isAdmin' => 'false',
22 | 'age' => '100',
23 | 'email' => 'user@provider.com',
24 | 'ip' => '192.168.105.22',
25 | ];
26 |
27 | $request = new Request(false);
28 | $request->setMethod('get');
29 | TestRouter::setRequest($request);
30 |
31 | TestRouter::get('/my/test/url', function (){
32 | $inputHandler = TestRouter::request()->getInputHandler();
33 | $data = $inputHandler->all(array(
34 | 'isAdmin',
35 | 'age'
36 | ));
37 | $this->assertEquals("false", $data['isAdmin']->getValue());
38 | $this->assertIsString($data['isAdmin']->getValue());
39 | $this->assertEquals("100", $data['age']->getValue());
40 | $this->assertIsString($data['age']->getValue());
41 | });
42 | TestRouter::debug('/my/test/url', 'get');
43 | }
44 |
45 | public function testInputParser()
46 | {
47 | TestRouter::resetRouter();
48 | global $_GET;
49 |
50 | $_GET = [
51 | 'fullname' => 'Max Mustermann',
52 | 'isAdmin' => 'false',
53 | 'isUser' => 'he is',
54 | 'age' => '100',
55 | 'email' => ' user@Provider.Com ',
56 | 'ip' => '192.168.105.22',
57 | 'ip2' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
58 | 'custom' => 'this is a text',
59 | 'custom2' => 'this is a text with: company'
60 | ];
61 |
62 | $request = new Request(false);
63 | $request->setMethod('get');
64 | TestRouter::setRequest($request);
65 |
66 | TestRouter::get('/my/test/url', function (){
67 | $inputHandler = TestRouter::request()->getInputHandler();
68 | $data = $inputHandler->all(array(
69 | 'isAdmin' => 'bool',
70 | 'isUser' => 'bool',
71 | 'age' => 'int',
72 | 'email' => 'email',
73 | 'ip' => 'ipv4',
74 | 'ip2' => 'ipv6',
75 | 'custom' => function(InputItem $inputItem){
76 | $inputItem->parser()->toString();
77 | return strpos($inputItem->getValue(), 'company') === false ? $inputItem->getValue() : 'illegal word';
78 | },
79 | 'custom2' => function(InputItem $inputItem){
80 | $inputItem->parser()->toString();
81 | return strpos($inputItem->getValue(), 'company') === false ? $inputItem->getValue() : 'illegal word';
82 | }
83 | ));
84 |
85 | $this->assertEquals(false, $data['isAdmin']->getValue());
86 | $this->assertIsBool($data['isAdmin']->getValue());
87 | $this->assertNull($data['isUser']->getValue());
88 | $this->assertEquals(100, $data['age']->getValue());
89 | $this->assertIsInt($data['age']->getValue());
90 | $this->assertEquals('user@provider.com', $data['email']->getValue());
91 | $this->assertIsString($data['email']->getValue());
92 | $this->assertEquals('192.168.105.22', $data['ip']->getValue());
93 | $this->assertIsString($data['ip']->getValue());
94 | $this->assertEquals('2001:0db8:85a3:0000:0000:8a2e:0370:7334', $data['ip2']->getValue());
95 | $this->assertIsString($data['ip2']->getValue());
96 | $this->assertNotEquals('illegal word', $data['custom']->getValue());
97 | $this->assertEquals('illegal word', $data['custom2']->getValue());
98 | });
99 | TestRouter::debug('/my/test/url', 'get');
100 | }
101 |
102 | public function testAttributeValidatorRules(){
103 | InputValidator::$parseAttributes = true;
104 | TestRouter::resetRouter();
105 | global $_POST;
106 |
107 | $_POST = [
108 | 'fullname' => 'Max Mustermann',
109 | 'isAdmin' => 'false',
110 | 'isUser' => 'he is',
111 | 'email' => 'user@provider.com',
112 | 'ip' => '192.168.105.22',
113 | ];
114 |
115 | $request = new Request(false);
116 | $request->setMethod('post');
117 | TestRouter::setRequest($request);
118 |
119 | TestRouter::post('/my/test/url', #[ValidatorAttribute('isAdmin', 'bool')] function (){
120 | $data = TestRouter::request()->getInputHandler()->requireAttributes();
121 | $this->assertEquals(false, $data['isAdmin']->getValue());
122 | $this->assertIsBool($data['isAdmin']->getValue());
123 | return 'success';
124 | });
125 |
126 | $output = TestRouter::debugOutput('/my/test/url', 'post');
127 |
128 | $this->assertEquals('success', $output);
129 |
130 | }
131 |
132 | }
--------------------------------------------------------------------------------
/src/Pecee/Http/Response.php:
--------------------------------------------------------------------------------
1 | request = $request;
23 | }
24 |
25 | /**
26 | * Set the http status code
27 | *
28 | * @param int $code
29 | * @return static
30 | */
31 | public function httpCode(int $code): self
32 | {
33 | http_response_code($code);
34 |
35 | return $this;
36 | }
37 |
38 | /**
39 | * Redirect the response
40 | *
41 | * @param string $url
42 | * @param int|null $httpCode
43 | */
44 | #[NoReturn]
45 | public function redirect(string $url, ?int $httpCode = null): void
46 | {
47 | if ($httpCode !== null) {
48 | $this->httpCode($httpCode);
49 | }
50 |
51 | $this->header('location: ' . $url);
52 | exit(0);
53 | }
54 |
55 | #[NoReturn]
56 | public function refresh(): void
57 | {
58 | $this->redirect($this->request->getUrl()->getOriginalUrl());
59 | }
60 |
61 | /**
62 | * Add http authorisation
63 | * @param string $name
64 | * @return static
65 | */
66 | public function auth(string $name = ''): self
67 | {
68 | $this->headers([
69 | 'WWW-Authenticate: Basic realm="' . $name . '"',
70 | 'HTTP/1.0 401 Unauthorized',
71 | ]);
72 |
73 | return $this;
74 | }
75 |
76 | /**
77 | * @param string $eTag
78 | * @param int $lastModifiedTime
79 | * @return $this
80 | */
81 | public function cache(string $eTag, int $lastModifiedTime = 2592000): self
82 | {
83 |
84 | $this->headers([
85 | 'Cache-Control: public',
86 | sprintf('Last-Modified: %s GMT', gmdate('D, d M Y H:i:s', $lastModifiedTime)),
87 | sprintf('Etag: %s', $eTag),
88 | ]);
89 |
90 | $httpModified = $this->request->getHeader('http-if-modified-since');
91 | $httpIfNoneMatch = $this->request->getHeader('http-if-none-match');
92 |
93 | if (($httpIfNoneMatch !== null && $httpIfNoneMatch === $eTag) || ($httpModified !== null && strtotime($httpModified) === $lastModifiedTime)) {
94 |
95 | $this->header('HTTP/1.1 304 Not Modified');
96 | exit(0);
97 | }
98 |
99 | return $this;
100 | }
101 |
102 | /**
103 | * Json encode
104 | * @param array|JsonSerializable $value
105 | * @param int $options JSON options Bitmask consisting of JSON_HEX_QUOT, JSON_HEX_TAG, JSON_HEX_AMP, JSON_HEX_APOS, JSON_NUMERIC_CHECK, JSON_PRETTY_PRINT, JSON_UNESCAPED_SLASHES, JSON_FORCE_OBJECT, JSON_PRESERVE_ZERO_FRACTION, JSON_UNESCAPED_UNICODE, JSON_PARTIAL_OUTPUT_ON_ERROR.
106 | * @param int $dept JSON debt.
107 | * @throws InvalidArgumentException
108 | */
109 | #[NoReturn]
110 | public function json(mixed $value, int $options = 0, int $dept = 512): void
111 | {
112 | if (($value instanceof JsonSerializable) === false && is_array($value) === false) {
113 | throw new InvalidArgumentException('Invalid type for parameter "value". Must be of type array or object implementing the \JsonSerializable interface.');
114 | }
115 |
116 | $this->header('Content-Type: application/json; charset=utf-8');
117 | echo json_encode($value, $options, $dept);
118 | exit(0);
119 | }
120 |
121 | /**
122 | * Add header to response
123 | * @param string $value
124 | * @return static
125 | */
126 | public function header(string $value): self
127 | {
128 | header($value);
129 |
130 | return $this;
131 | }
132 |
133 | /**
134 | * Add multiple headers to response
135 | * @param array $headers
136 | * @return static
137 | */
138 | public function headers(array $headers): self
139 | {
140 | foreach ($headers as $header) {
141 | $this->header($header);
142 | }
143 |
144 | return $this;
145 | }
146 |
147 | /**
148 | * @return static
149 | */
150 | #[NoReturn]
151 | public function back(): self
152 | {
153 | $referer = $this->request->getReferer();
154 |
155 | if ($referer === null) {
156 | $this->refresh();
157 | }
158 |
159 | $this->redirect($referer);
160 | }
161 |
162 | /**
163 | * @param string $raw
164 | * @return void
165 | */
166 | #[NoReturn]
167 | public function raw(string $raw): void
168 | {
169 | echo $raw;
170 | exit(0);
171 | }
172 |
173 | /**
174 | * @param string $filename
175 | * @return void
176 | */
177 | #[NoReturn]
178 | public function render(string $filename): void
179 | {
180 | $this->raw(file_get_contents($filename));
181 | }
182 |
183 | /**
184 | * @requires https://github.com/erusev/parsedown
185 | *
186 | * @param string $code
187 | * @param bool $save_mode
188 | * @return void
189 | */
190 | #[NoReturn]
191 | public function markdown(string $code, bool $save_mode = false): void
192 | {
193 | if(!class_exists('\Parsedown')){
194 | $this->raw($code);
195 | }
196 | $parsedown = new \Parsedown();
197 | $parsedown->setSafeMode($save_mode);
198 |
199 | $this->raw($parsedown->text($code));
200 | }
201 |
202 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Route/RouteController.php:
--------------------------------------------------------------------------------
1 | setUrl($url);
21 | $this->setName(trim(str_replace('/', '.', $url), '/'));
22 | $this->controller = $controller;
23 | }
24 |
25 | /**
26 | * Check if route has given name.
27 | *
28 | * @param string $name
29 | * @return bool
30 | */
31 | public function hasName(string $name): bool
32 | {
33 | if ($this->name === null) {
34 | return false;
35 | }
36 |
37 | /* Remove method/type */
38 | if (str_contains($name, '.')) {
39 | $method = substr($name, strrpos($name, '.') + 1);
40 | $newName = substr($name, 0, strrpos($name, '.'));
41 |
42 | if (in_array($method, $this->names, true) === true && strtolower($this->name) === strtolower($newName)) {
43 | return true;
44 | }
45 | }
46 |
47 | return parent::hasName($name);
48 | }
49 |
50 | /**
51 | * @param string|null $method
52 | * @param array|string|null $parameters
53 | * @param string|null $name
54 | * @return string
55 | */
56 | public function findUrl(?string $method = null, array|string $parameters = null, ?string $name = null): string
57 | {
58 | if (str_contains($name, '.')) {
59 | $found = array_search(substr($name, strrpos($name, '.') + 1), $this->names, true);
60 | if ($found !== false) {
61 | $method = (string)$found;
62 | }
63 | }
64 |
65 | $url = '';
66 | $parameters = (array)$parameters;
67 |
68 | if ($method !== null) {
69 |
70 | /* Remove requestType from method-name, if it exists */
71 | foreach (Request::$requestTypes as $requestType) {
72 |
73 | if (stripos($method, $requestType) === 0) {
74 | $method = substr($method, strlen($requestType));
75 | break;
76 | }
77 | }
78 |
79 | $method .= '/';
80 | }
81 |
82 | $group = $this->getGroup();
83 |
84 | if ($group !== null && count($group->getDomains()) !== 0) {
85 | $url .= '//' . $group->getDomains()[0];
86 | }
87 |
88 | $url .= '/' . trim($this->getUrl(), '/') . '/' . strtolower($method ?? '') . implode('/', $parameters);
89 |
90 | return '/' . trim($url, '/') . '/';
91 | }
92 |
93 | /**
94 | * @param string $url
95 | * @param Request $request
96 | * @return bool
97 | */
98 | public function matchRoute(string $url, Request $request): bool
99 | {
100 | if ($this->matchGroup($url, $request) === false) {
101 | return false;
102 | }
103 |
104 | /* Match global regular-expression for route */
105 | $regexMatch = $this->matchRegex($request, $url);
106 |
107 | if ($regexMatch === false || (stripos($url, $this->url) !== 0 && strtoupper($url) !== strtoupper($this->url))) {
108 | return false;
109 | }
110 |
111 | $strippedUrl = trim(str_ireplace($this->url, '/', $url), '/');
112 | $path = explode('/', $strippedUrl);
113 |
114 | if (count($path) !== 0) {
115 |
116 | $method = (isset($path[0]) === false || trim($path[0]) === '') ? $this->defaultMethod : $path[0];
117 | $this->method = $request->getMethod() . ucfirst($method);
118 |
119 | $this->parameters = array_slice($path, 1);
120 |
121 | // Set callback
122 | $this->setCallback([$this->controller, $this->method]);
123 |
124 | return true;
125 | }
126 |
127 | return false;
128 | }
129 |
130 | /**
131 | * Get controller class-name.
132 | *
133 | * @return string
134 | */
135 | public function getController(): string
136 | {
137 | return $this->controller;
138 | }
139 |
140 | /**
141 | * Get controller class-name.
142 | *
143 | * @param string $controller
144 | * @return static
145 | */
146 | public function setController(string $controller): IControllerRoute
147 | {
148 | $this->controller = $controller;
149 |
150 | return $this;
151 | }
152 |
153 | /**
154 | * Return active method
155 | *
156 | * @return string|null
157 | */
158 | public function getMethod(): ?string
159 | {
160 | return $this->method;
161 | }
162 |
163 | /**
164 | * Set active method
165 | *
166 | * @param string $method
167 | * @return static
168 | */
169 | public function setMethod(string $method): IRoute
170 | {
171 | $this->method = $method;
172 |
173 | return $this;
174 | }
175 |
176 | /**
177 | * Merge with information from another route.
178 | *
179 | * @param array $settings
180 | * @param bool $merge
181 | * @return static
182 | */
183 | public function setSettings(array $settings, bool $merge = false): IRoute
184 | {
185 | if (isset($settings['names']) === true) {
186 | $this->names = $settings['names'];
187 | }
188 |
189 | return parent::setSettings($settings, $merge);
190 | }
191 |
192 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Handlers/EventHandler.php:
--------------------------------------------------------------------------------
1 | registeredEvents[$name]) === true) {
141 | $this->registeredEvents[$name][] = $callback;
142 | } else {
143 | $this->registeredEvents[$name] = [$callback];
144 | }
145 |
146 | return $this;
147 | }
148 |
149 | /**
150 | * Get events.
151 | *
152 | * @param string|null $name Filter events by name.
153 | * @param array|string ...$names Add multiple names...
154 | * @return array
155 | */
156 | public function getEvents(?string $name, ...$names): array
157 | {
158 | if ($name === null) {
159 | return $this->registeredEvents;
160 | }
161 |
162 | $names[] = $name;
163 | $events = [];
164 |
165 | foreach ($names as $eventName) {
166 | if (isset($this->registeredEvents[$eventName]) === true) {
167 | $events += $this->registeredEvents[$eventName];
168 | }
169 | }
170 |
171 | return $events;
172 | }
173 |
174 | /**
175 | * Fires any events registered with given event-name
176 | *
177 | * @param Router $router Router instance
178 | * @param string $name Event name
179 | * @param array $eventArgs Event arguments
180 | */
181 | public function fireEvents(Router $router, string $name, array $eventArgs = []): void
182 | {
183 | $events = $this->getEvents(static::EVENT_ALL, $name);
184 |
185 | /* @var $event Closure */
186 | foreach ($events as $event) {
187 | $event(new EventArgument($name, $router, $eventArgs));
188 | }
189 | }
190 |
191 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/RouterRewriteTest.php:
--------------------------------------------------------------------------------
1 | [ExceptionHandlerFirst::class, ExceptionHandlerSecond::class]], function () {
37 |
38 | TestRouter::group(['prefix' => '/test', 'exceptionHandler' => ExceptionHandlerThird::class], function () {
39 |
40 | TestRouter::get('/my-path', 'DummyController@method1');
41 |
42 | });
43 | });
44 |
45 | try {
46 | TestRouter::debug('/test/non-existing', 'get');
47 | } catch (\ResponseException $e) {
48 |
49 | }
50 |
51 | $expectedStack = [
52 | ExceptionHandlerThird::class,
53 | ExceptionHandlerSecond::class,
54 | ExceptionHandlerFirst::class,
55 | ];
56 |
57 | $this->assertEquals($expectedStack, $stack);
58 |
59 | }
60 |
61 | public function testStopMergeExceptionHandlers()
62 | {
63 | global $stack;
64 | $stack = [];
65 |
66 | TestRouter::group(['prefix' => '/', 'exceptionHandler' => ExceptionHandlerFirst::class], function () {
67 |
68 | TestRouter::group(['prefix' => '/admin', 'exceptionHandler' => ExceptionHandlerSecond::class, 'mergeExceptionHandlers' => false], function () {
69 |
70 | TestRouter::get('/my-path', 'DummyController@method1');
71 |
72 | });
73 | });
74 |
75 | try {
76 | TestRouter::debug('/admin/my-path-test', 'get');
77 | } catch (\Pecee\SimpleRouter\Exceptions\NotFoundHttpException $e) {
78 |
79 | }
80 |
81 | $expectedStack = [
82 | ExceptionHandlerSecond::class,
83 | ];
84 |
85 | $this->assertEquals($expectedStack, $stack);
86 | }
87 |
88 | public function testRewriteExceptionMessage()
89 | {
90 | $this->expectException(\Pecee\SimpleRouter\Exceptions\NotFoundHttpException::class);
91 |
92 | TestRouter::error(function (\Pecee\Http\Request $request, \Exception $error) {
93 |
94 | if (strtolower($request->getUrl()->getPath()) === '/my/test/') {
95 | $request->setRewriteUrl('/another-non-existing');
96 | }
97 |
98 | });
99 |
100 | TestRouter::debug('/my/test', 'get');
101 | }
102 |
103 | public function testRewriteUrlFromRoute()
104 | {
105 |
106 | TestRouter::get('/old', function () {
107 | TestRouter::request()->setRewriteUrl('/new');
108 | });
109 |
110 | TestRouter::get('/new', function () {
111 | echo 'ok';
112 | });
113 |
114 | TestRouter::get('/new1', function () {
115 | echo 'ok';
116 | });
117 |
118 | TestRouter::get('/new2', function () {
119 | echo 'ok';
120 | });
121 |
122 | $output = TestRouter::debugOutput('/old');
123 |
124 | $this->assertEquals('ok', $output);
125 |
126 | }
127 |
128 | public function testRewriteCallbackFromRoute()
129 | {
130 |
131 | TestRouter::get('/old', function () {
132 | TestRouter::request()->setRewriteUrl('/new');
133 | });
134 |
135 | TestRouter::get('/new', function () {
136 | return 'ok';
137 | });
138 |
139 | TestRouter::get('/new1', function () {
140 | return 'fail';
141 | });
142 |
143 | TestRouter::get('/new/2', function () {
144 | return 'fail';
145 | });
146 |
147 | $output = TestRouter::debugOutput('/old');
148 |
149 | TestRouter::resetRouter();
150 |
151 | $this->assertEquals('ok', $output);
152 |
153 | }
154 |
155 | public function testRewriteRouteFromRoute()
156 | {
157 |
158 | TestRouter::get('/match', function () {
159 | TestRouter::request()->setRewriteRoute(new \Pecee\SimpleRouter\Route\RouteUrl('/match', function () {
160 | return 'ok';
161 | }));
162 | });
163 |
164 | TestRouter::get('/old1', function () {
165 | return 'fail';
166 | });
167 |
168 | TestRouter::get('/old/2', function () {
169 | return 'fail';
170 | });
171 |
172 | TestRouter::get('/new2', function () {
173 | return 'fail';
174 | });
175 |
176 | $output = TestRouter::debugOutput('/match');
177 |
178 | TestRouter::resetRouter();
179 |
180 | $this->assertEquals('ok', $output);
181 |
182 | }
183 |
184 | public function testMiddlewareRewrite()
185 | {
186 |
187 | TestRouter::group(['middleware' => 'RewriteMiddleware'], function () {
188 | TestRouter::get('/', function () {
189 | return 'fail';
190 | });
191 |
192 | TestRouter::get('no/match', function () {
193 | return 'fail';
194 | });
195 | });
196 |
197 | $output = TestRouter::debugOutput('/');
198 |
199 | $this->assertEquals('ok', $output);
200 |
201 | }
202 |
203 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Route/IRoute.php:
--------------------------------------------------------------------------------
1 | '',
11 | 'create' => 'create',
12 | 'store' => '',
13 | 'show' => '',
14 | 'edit' => 'edit',
15 | 'update' => '',
16 | 'destroy' => '',
17 | ];
18 |
19 | protected array $methodNames = [
20 | 'index' => 'index',
21 | 'create' => 'create',
22 | 'store' => 'store',
23 | 'show' => 'show',
24 | 'edit' => 'edit',
25 | 'update' => 'update',
26 | 'destroy' => 'destroy',
27 | ];
28 |
29 | protected array $names = [];
30 | protected string $controller;
31 |
32 | /**
33 | * @param string $url
34 | * @param string $controller
35 | */
36 | public function __construct(string $url, string $controller)
37 | {
38 | $this->setUrl($url);
39 | $this->controller = $controller;
40 | $this->setName(trim(str_replace('/', '.', $url), '/'));
41 | }
42 |
43 | /**
44 | * Check if route has given name.
45 | *
46 | * @param string $name
47 | * @return bool
48 | */
49 | public function hasName(string $name): bool
50 | {
51 | if ($this->getName() === null) {
52 | return false;
53 | }
54 |
55 | if (strtolower($this->getName()) === strtolower($name)) {
56 | return true;
57 | }
58 |
59 | /* Remove method/type */
60 | if (str_contains($name, '.')) {
61 | $name = substr($name, 0, strrpos($name, '.'));
62 | }
63 |
64 | return (strtolower($this->getName()) === strtolower($name));
65 | }
66 |
67 | /**
68 | * @param string|null $method
69 | * @param array|string|null $parameters
70 | * @param string|null $name
71 | * @return string
72 | */
73 | public function findUrl(?string $method = null, array|string $parameters = null, ?string $name = null): string
74 | {
75 | $url = array_search($name, $this->names, true);
76 | if ($url !== false) {
77 | return rtrim($this->url . $this->urls[$url], '/') . '/';
78 | }
79 |
80 | return $this->url;
81 | }
82 |
83 | protected function call(string $method): bool
84 | {
85 | $this->setCallback([$this->controller, $method]);
86 |
87 | return true;
88 | }
89 |
90 | public function matchRoute(string $url, Request $request): bool
91 | {
92 | if ($this->matchGroup($url, $request) === false) {
93 | return false;
94 | }
95 |
96 | /* Match global regular-expression for route */
97 | $regexMatch = $this->matchRegex($request, $url);
98 |
99 | if ($regexMatch === false || (stripos($url, $this->url) !== 0 && strtoupper($url) !== strtoupper($this->url))) {
100 | return false;
101 | }
102 |
103 | $route = rtrim($this->url, '/') . '/{id?}/{action?}';
104 |
105 | /* Parse parameters from current route */
106 | $this->parameters = $this->parseParameters($route, $url);
107 |
108 | /* If no custom regular expression or parameters was found on this route, we stop */
109 | if ($regexMatch === null && $this->getParameters() === null) {
110 | return false;
111 | }
112 | $action = strtolower(trim($this->getParameters()['action'] ?? ''));
113 | $id = $this->getParameters()['id'];
114 |
115 | // Remove action parameter
116 | unset($this->getParameters()['action']);
117 |
118 | $method = $request->getMethod();
119 |
120 | // Delete
121 | if ($method === Request::REQUEST_TYPE_DELETE && $id !== null) {
122 | return $this->call($this->getMethodNames()['destroy']);
123 | }
124 |
125 | // Update
126 | if ($id !== null && in_array($method, [Request::REQUEST_TYPE_PATCH, Request::REQUEST_TYPE_PUT], true) === true) {
127 | return $this->call($this->getMethodNames()['update']);
128 | }
129 |
130 | // Edit
131 | if ($method === Request::REQUEST_TYPE_GET && $id !== null && $action === 'edit') {
132 | return $this->call($this->getMethodNames()['edit']);
133 | }
134 |
135 | // Create
136 | if ($method === Request::REQUEST_TYPE_GET && $id === 'create') {
137 | return $this->call($this->getMethodNames()['create']);
138 | }
139 |
140 | // Save
141 | if ($method === Request::REQUEST_TYPE_POST) {
142 | return $this->call($this->getMethodNames()['store']);
143 | }
144 |
145 | // Show
146 | if ($method === Request::REQUEST_TYPE_GET && $id !== null) {
147 | return $this->call($this->getMethodNames()['show']);
148 | }
149 |
150 | // Index
151 | return $this->call($this->getMethodNames()['index']);
152 | }
153 |
154 | /**
155 | * @return string
156 | */
157 | public function getController(): string
158 | {
159 | return $this->controller;
160 | }
161 |
162 | /**
163 | * @param string $controller
164 | * @return static
165 | */
166 | public function setController(string $controller): IControllerRoute
167 | {
168 | $this->controller = $controller;
169 |
170 | return $this;
171 | }
172 |
173 | public function setName(string $name): ILoadableRoute
174 | {
175 | $this->name = $name;
176 |
177 | $this->names = [
178 | 'index' => $this->name . '.index',
179 | 'create' => $this->name . '.create',
180 | 'store' => $this->name . '.store',
181 | 'show' => $this->name . '.show',
182 | 'edit' => $this->name . '.edit',
183 | 'update' => $this->name . '.update',
184 | 'destroy' => $this->name . '.destroy',
185 | ];
186 |
187 | return $this;
188 | }
189 |
190 | /**
191 | * Define custom method name for resource controller
192 | *
193 | * @param array $names
194 | * @return static $this
195 | */
196 | public function setMethodNames(array $names): RouteResource
197 | {
198 | $this->methodNames = $names;
199 |
200 | return $this;
201 | }
202 |
203 | /**
204 | * Get method names
205 | *
206 | * @return array
207 | */
208 | public function getMethodNames(): array
209 | {
210 | return $this->methodNames;
211 | }
212 |
213 | /**
214 | * Merge with information from another route.
215 | *
216 | * @param array $settings
217 | * @param bool $merge
218 | * @return static
219 | */
220 | public function setSettings(array $settings, bool $merge = false): IRoute
221 | {
222 | if (isset($settings['names']) === true) {
223 | $this->names = $settings['names'];
224 | }
225 |
226 | if (isset($settings['methods']) === true) {
227 | $this->methodNames = $settings['methods'];
228 | }
229 |
230 | return parent::setSettings($settings, $merge);
231 | }
232 |
233 | }
--------------------------------------------------------------------------------
/src/Pecee/Http/Input/InputParser.php:
--------------------------------------------------------------------------------
1 | inputItem = $inputItem;
19 | }
20 |
21 | /**
22 | * @return mixed
23 | */
24 | public function getValue()
25 | {
26 | return $this->getInputItem()->getValue();
27 | }
28 |
29 | /**
30 | * @param callable|string $setting
31 | * @return self
32 | */
33 | public function parseFromSetting(callable|string $setting): self
34 | {
35 | if(is_callable($setting)){
36 | $this->setValue($setting($this->getInputItem()));
37 | }else{
38 | if($this->getValue() === null)
39 | return $this;
40 | switch($setting){
41 | case 'string':
42 | $this->toString();
43 | break;
44 | case 'integer':
45 | case 'int':
46 | $this->toInteger();
47 | break;
48 | case 'float':
49 | $this->toFloat();
50 | break;
51 | case 'boolean':
52 | case 'bool':
53 | $this->toBoolean();
54 | break;
55 | case 'email':
56 | $this->sanitizeEmailAddress();
57 | break;
58 | case 'ip':
59 | $this->toIp();
60 | break;
61 | case 'ipv4':
62 | $this->toIp('ipv4');
63 | break;
64 | case 'ipv6':
65 | $this->toIp('ipv6');
66 | break;
67 | case 'mixed':
68 | //Nothing
69 | break;
70 | }
71 | }
72 | return $this;
73 | }
74 |
75 | /**
76 | * @param mixed $value
77 | * @return self
78 | */
79 | private function setValue(mixed $value): self
80 | {
81 | $this->getInputItem()->setValue($value);
82 | return $this;
83 | }
84 |
85 | /**
86 | * @return InputItem
87 | */
88 | public function getInputItem(): InputItem
89 | {
90 | return $this->inputItem;
91 | }
92 |
93 | /* https://www.php.net/manual/de/filter.filters.validate.php */
94 |
95 | /**
96 | * @return bool|null
97 | */
98 | public function toBoolean(): ?bool
99 | {
100 | $this->setValue(filter_var($this->getValue(), FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE));
101 | return $this->getValue();
102 | }
103 |
104 | /**
105 | * @return string
106 | */
107 | public function toDomain(): string
108 | {
109 | $this->setValue(filter_var($this->getValue(), FILTER_VALIDATE_DOMAIN, FILTER_NULL_ON_FAILURE));
110 | return $this->getValue();
111 | }
112 |
113 | /**
114 | * @return string
115 | */
116 | public function toEmail(): string
117 | {
118 | $this->setValue(filter_var($this->getValue(), FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE));
119 | return $this->getValue();
120 | }
121 |
122 | /**
123 | * @return float|null
124 | */
125 | public function toFloat(): ?float
126 | {
127 | $this->setValue(filter_var($this->getValue(), FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE));
128 | return $this->getValue();
129 | }
130 |
131 | /**
132 | * @return int|null
133 | */
134 | public function toInteger(): ?int
135 | {
136 | $this->setValue(filter_var($this->getValue(), FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE));
137 | return $this->getValue();
138 | }
139 |
140 | /**
141 | * @param string|null $type ipv4, ipv6 or both
142 | * @return string|null
143 | */
144 | public function toIp(?string $type = null): ?string
145 | {
146 | switch($type){
147 | case 'ipv4':
148 | $this->setValue(filter_var($this->getValue(), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_NULL_ON_FAILURE));
149 | break;
150 | case 'ipv6':
151 | $this->setValue(filter_var($this->getValue(), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | FILTER_NULL_ON_FAILURE));
152 | break;
153 | default:
154 | $this->setValue(filter_var($this->getValue(), FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE));
155 | break;
156 | }
157 | return $this->getValue();
158 | }
159 |
160 | /* Other types */
161 |
162 | /**
163 | * @return string
164 | */
165 | public function toString(): string
166 | {
167 | $this->setValue(strval($this->getValue()));
168 | return $this->getValue();
169 | }
170 |
171 | /* Other */
172 |
173 | /**
174 | * @param array $allowed_tags
175 | * @return self
176 | */
177 | public function stripTags(array $allowed_tags = array()): self
178 | {
179 | return $this->setValue(strip_tags($this->getValue(), $allowed_tags));
180 | }
181 |
182 | /**
183 | * @return self
184 | */
185 | public function stripTags2(): self
186 | {
187 | return $this->setValue(filter_var($this->getValue(), FILTER_SANITIZE_STRING));
188 | }
189 |
190 | /**
191 | * @return self
192 | */
193 | public function htmlSpecialChars(): self
194 | {
195 | return $this->setValue(htmlspecialchars($this->getValue(), ENT_QUOTES | ENT_HTML5));
196 | }
197 |
198 | /**
199 | * Best practise with Inputs from Users
200 | *
201 | * @return self
202 | */
203 | public function sanitize(): self
204 | {
205 | return $this->htmlSpecialChars();
206 | }
207 |
208 | /**
209 | * @return self
210 | */
211 | public function sanitizeEmailAddress(): self
212 | {
213 | $this->toLower()->trim()->toEmail();
214 | return $this;
215 | }
216 |
217 | /**
218 | * @return self
219 | */
220 | public function urlEncode(): self
221 | {
222 | $this->setValue(urlencode($this->getValue()));
223 | return $this;
224 | }
225 |
226 | /**
227 | * @return self
228 | */
229 | public function base64Encode(): self
230 | {
231 | $this->setValue(base64_encode($this->getValue()));
232 | return $this;
233 | }
234 |
235 | /**
236 | * @return self
237 | */
238 | public function toLower(): self
239 | {
240 | $this->setValue(strtolower($this->getValue()));
241 | return $this;
242 | }
243 |
244 | /**
245 | * @return self
246 | */
247 | public function toUpper(): self
248 | {
249 | $this->setValue(strtoupper($this->getValue()));
250 | return $this;
251 | }
252 |
253 | /**
254 | * @return self
255 | */
256 | public function trim(): self
257 | {
258 | $this->setValue(trim($this->getValue()));
259 | return $this;
260 | }
261 |
262 | /**
263 | * Credits: https://stackoverflow.com/questions/2791998/convert-string-with-dashes-to-camelcase#answer-2792045
264 | * @param string $separator
265 | * @param bool $capitalizeFirstCharacter
266 | * @return self
267 | */
268 | function toCamelCase(string $separator = '_', bool $capitalizeFirstCharacter = false): self
269 | {
270 | $value = str_replace('-', '', ucwords($this->getValue(), $separator));
271 |
272 | if (!$capitalizeFirstCharacter) {
273 | $value = lcfirst($value);
274 | }
275 | $this->setValue($value);
276 |
277 | return $this;
278 | }
279 |
280 | /**
281 | * @param string $separator
282 | * @return self
283 | */
284 | function fromCamelCase(string $separator = '_'): self
285 | {
286 | $value = lcfirst($this->getValue());
287 |
288 | $this->setValue(preg_replace_callback('/[A-Z]/', function($value){
289 | return '_' . strtolower($value[0]);
290 | }, $value));
291 |
292 | return $this;
293 | }
294 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Route/RouteGroup.php:
--------------------------------------------------------------------------------
1 | domains) === 0) {
26 | return true;
27 | }
28 |
29 | foreach ($this->domains as $domain) {
30 |
31 | // If domain has no parameters but matches
32 | if ($domain === $request->getHost()) {
33 | return true;
34 | }
35 |
36 | $parameters = $this->parseParameters($domain, $request->getHost(), '.*');
37 |
38 | if ($parameters !== null && count($parameters) !== 0) {
39 | $this->parameters = $parameters;
40 |
41 | return true;
42 | }
43 | }
44 |
45 | return false;
46 | }
47 |
48 | /**
49 | * Method called to check if route matches
50 | *
51 | * @param string $url
52 | * @param Request $request
53 | * @return bool
54 | */
55 | public function matchRoute(string $url, Request $request): bool
56 | {
57 | if ($this->getGroup() !== null && $this->getGroup()->matchRoute($url, $request) === false) {
58 | return false;
59 | }
60 |
61 | if ($this->getPrefix() !== null) {
62 | /* Parse parameters from current route */
63 | $parameters = $this->parseParameters($this->getPrefix(), $url);
64 |
65 | /* If no custom regular expression or parameters was found on this route, we stop */
66 | if ($parameters === null) {
67 | return false;
68 | }
69 |
70 | /* Set the parameters */
71 | $this->setParameters($parameters);
72 | }
73 |
74 | $parsedPrefix = $this->getPrefix();
75 |
76 | if($parsedPrefix !== null){
77 | foreach($this->getParameters() as $parameter => $value){
78 | $parsedPrefix = str_ireplace('{' . $parameter . '}', $value, $parsedPrefix);
79 | }
80 | }
81 |
82 | /* Skip if prefix doesn't match */
83 | if ($this->getPrefix() !== null && stripos($url, rtrim($parsedPrefix, '/') . '/') === false) {
84 | return false;
85 | }
86 |
87 | return $this->matchDomain($request);
88 | }
89 |
90 | /**
91 | * Add exception handler
92 | *
93 | * @param string|IExceptionHandler $handler
94 | * @return static
95 | */
96 | public function addExceptionHandler(IExceptionHandler|string $handler): static
97 | {
98 | $this->exceptionHandlers[] = $handler;
99 |
100 | return $this;
101 | }
102 |
103 | /**
104 | * Set exception-handlers for group
105 | *
106 | * @param array $handlers
107 | * @return static
108 | */
109 | public function setExceptionHandlers(array $handlers): static
110 | {
111 | $this->exceptionHandlers = $handlers;
112 |
113 | return $this;
114 | }
115 |
116 | /**
117 | * Get exception-handlers for group
118 | *
119 | * @return array
120 | */
121 | public function getExceptionHandlers(): array
122 | {
123 | return $this->exceptionHandlers;
124 | }
125 |
126 | /**
127 | * Get allowed domains for domain.
128 | *
129 | * @return array
130 | */
131 | public function getDomains(): array
132 | {
133 | return $this->domains;
134 | }
135 |
136 | /**
137 | * Set allowed domains for group.
138 | *
139 | * @param array $domains
140 | * @return static
141 | */
142 | public function setDomains(array $domains): static
143 | {
144 | $this->domains = $domains;
145 |
146 | return $this;
147 | }
148 |
149 | /**
150 | * @param string $prefix
151 | * @return static
152 | */
153 | public function setPrefix(string $prefix): static
154 | {
155 | $this->prefix = '/' . trim($prefix, '/');
156 |
157 | return $this;
158 | }
159 |
160 | /**
161 | * Prepends prefix while ensuring that the url has the correct formatting.
162 | *
163 | * @param string $url
164 | * @return static
165 | */
166 | public function prependPrefix(string $url): static
167 | {
168 | return $this->setPrefix(rtrim($url, '/') . ($this->getPrefix() ?? ''));
169 | }
170 |
171 | /**
172 | * Get prefix that child-routes will inherit.
173 | *
174 | * @return string|null
175 | */
176 | public function getPrefix(): ?string
177 | {
178 | return $this->prefix;
179 | }
180 |
181 | /**
182 | * Sets the router name, which makes it easier to obtain the url or router at a later point.
183 | * Alias for RouteGroup::setName().
184 | *
185 | * @param string $name
186 | * @return static
187 | * @see RouteGroup::setName()
188 | */
189 | public function name(array|string $name): IGroupRoute
190 | {
191 | return $this->setName($name);
192 | }
193 |
194 | /**
195 | * @param string $name
196 | * @return static
197 | */
198 | public function setName(string $name): static
199 | {
200 | $this->name = $name;
201 |
202 | return $this;
203 | }
204 |
205 | /**
206 | * Get name
207 | *
208 | * @return string|null
209 | */
210 | public function getName(): ?string
211 | {
212 | return $this->name;
213 | }
214 |
215 | /**
216 | * When enabled group will overwrite any existing exception-handlers.
217 | *
218 | * @param bool $merge
219 | * @return static
220 | */
221 | public function setMergeExceptionHandlers(bool $merge): static
222 | {
223 | $this->mergeExceptionHandlers = $merge;
224 |
225 | return $this;
226 | }
227 |
228 | /**
229 | * Returns true if group should overwrite existing exception-handlers.
230 | *
231 | * @return bool
232 | */
233 | public function getMergeExceptionHandlers(): bool
234 | {
235 | return $this->mergeExceptionHandlers;
236 | }
237 |
238 | /**
239 | * Merge with information from another route.
240 | *
241 | * @param array $settings
242 | * @param bool $merge
243 | * @return static
244 | */
245 | public function setSettings(array $settings, bool $merge = false): static
246 | {
247 | if (isset($settings['prefix']) === true) {
248 | $this->setPrefix($settings['prefix'] . ($this->getPrefix() ?? ''));
249 | }
250 |
251 | if (isset($settings['mergeExceptionHandlers']) === true) {
252 | $this->setMergeExceptionHandlers($settings['mergeExceptionHandlers']);
253 | }
254 |
255 | if ($merge === false && isset($settings['exceptionHandler']) === true) {
256 | $this->setExceptionHandlers((array)$settings['exceptionHandler']);
257 | }
258 |
259 | if ($merge === false && isset($settings['domain']) === true) {
260 | $this->setDomains((array)$settings['domain']);
261 | }
262 |
263 | if (isset($settings['as']) === true) {
264 |
265 | $name = $settings['as'];
266 |
267 | if ($this->getName() !== null && $merge !== false) {
268 | $name .= '.' . $this->getName();
269 | }
270 |
271 | $this->setName($name);
272 | }
273 |
274 | return parent::setSettings($settings, $merge);
275 | }
276 |
277 | /**
278 | * Export route settings to array so they can be merged with another route.
279 | *
280 | * @return array
281 | */
282 | public function toArray(): array
283 | {
284 | $values = [];
285 |
286 | if ($this->getPrefix() !== null) {
287 | $values['prefix'] = $this->getPrefix();
288 | }
289 |
290 | if ($this->getName() !== null) {
291 | $values['as'] = $this->getName();
292 | }
293 |
294 | if (count($this->getParameters()) !== 0) {
295 | $values['parameters'] = $this->getParameters();
296 | }
297 |
298 | return array_merge($values, parent::toArray());
299 | }
300 |
301 | }
302 |
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/RouterRouteTest.php:
--------------------------------------------------------------------------------
1 | assertTrue($result);
26 | }
27 |
28 | public function testMultiParam()
29 | {
30 | $result = false;
31 | TestRouter::get('/test-{param1}-{param2}', function ($param1, $param2) use (&$result) {
32 |
33 | if ($param1 === 'param1' && $param2 === 'param2') {
34 | $result = true;
35 | }
36 |
37 | });
38 |
39 | TestRouter::debug('/test-param1-param2', 'get');
40 |
41 | $this->assertTrue($result);
42 |
43 | }
44 |
45 | public function testNotFound()
46 | {
47 | $this->expectException('\Pecee\SimpleRouter\Exceptions\NotFoundHttpException');
48 | TestRouter::get('/non-existing-path', 'DummyController@method1');
49 | TestRouter::debug('/test-param1-param2', 'post');
50 | }
51 |
52 | public function testGet()
53 | {
54 | TestRouter::get('/my/test/url', 'DummyController@method1');
55 | TestRouter::debug('/my/test/url', 'get');
56 |
57 | $this->assertTrue(true);
58 | }
59 |
60 | public function testPost()
61 | {
62 | TestRouter::post('/my/test/url', 'DummyController@method1');
63 | TestRouter::debug('/my/test/url', 'post');
64 |
65 | $this->assertTrue(true);
66 | }
67 |
68 | public function testPut()
69 | {
70 | TestRouter::put('/my/test/url', 'DummyController@method1');
71 | TestRouter::debug('/my/test/url', 'put');
72 |
73 | $this->assertTrue(true);
74 | }
75 |
76 | public function testDelete()
77 | {
78 | TestRouter::delete('/my/test/url', 'DummyController@method1');
79 | TestRouter::debug('/my/test/url', 'delete');
80 |
81 | $this->assertTrue(true);
82 | }
83 |
84 | public function testMethodNotAllowed()
85 | {
86 | TestRouter::get('/my/test/url', 'DummyController@method1');
87 |
88 | try {
89 | TestRouter::debug('/my/test/url', 'post');
90 | } catch (\Exception $e) {
91 | $this->assertEquals(403, $e->getCode());
92 | }
93 | }
94 |
95 | public function testSimpleParam()
96 | {
97 | TestRouter::get('/test-{param1}', 'DummyController@param');
98 | $response = TestRouter::debugOutput('/test-param1', 'get');
99 |
100 | $this->assertEquals('param1', $response);
101 | }
102 |
103 | public function testPathParamRegex()
104 | {
105 | TestRouter::get('/{lang}/productscategories/{name}', 'DummyController@param', ['where' => ['lang' => '[a-z]+', 'name' => '[A-Za-z0-9-]+']]);
106 | $response = TestRouter::debugOutput('/it/productscategories/system', 'get');
107 |
108 | $this->assertEquals('it, system', $response);
109 | }
110 |
111 | public function testFixedDomain()
112 | {
113 | $result = false;
114 | TestRouter::request()->setHost('admin.world.com');
115 |
116 | TestRouter::group(['domain' => 'admin.world.com'], function () use (&$result) {
117 | TestRouter::get('/test', function ($subdomain = null) use (&$result) {
118 | $result = true;
119 | });
120 | });
121 |
122 | TestRouter::debug('/test', 'get');
123 |
124 | $this->assertTrue($result);
125 | }
126 |
127 | public function testFixedNotAllowedDomain()
128 | {
129 | $result = false;
130 | TestRouter::request()->setHost('other.world.com');
131 |
132 | TestRouter::group(['domain' => 'admin.world.com'], function () use (&$result) {
133 | TestRouter::get('/', function ($subdomain = null) use (&$result) {
134 | $result = true;
135 | });
136 | });
137 |
138 | try {
139 | TestRouter::debug('/', 'get');
140 | } catch(\Exception $e) {
141 |
142 | }
143 |
144 | $this->assertFalse($result);
145 | }
146 |
147 | public function testDomainAllowedRoute()
148 | {
149 | $result = false;
150 | TestRouter::request()->setHost('hello.world.com');
151 |
152 | TestRouter::group(['domain' => '{subdomain}.world.com'], function () use (&$result) {
153 | TestRouter::get('/test', function ($subdomain = null) use (&$result) {
154 | $result = ($subdomain === 'hello');
155 | });
156 | });
157 |
158 | TestRouter::debug('/test', 'get');
159 |
160 | $this->assertTrue($result);
161 |
162 | }
163 |
164 | public function testDomainNotAllowedRoute()
165 | {
166 | TestRouter::request()->setHost('other.world.com');
167 |
168 | $result = false;
169 |
170 | TestRouter::group(['domain' => '{subdomain}.world.com'], function () use (&$result) {
171 | TestRouter::get('/test', function ($subdomain = null) use (&$result) {
172 | $result = ($subdomain === 'hello');
173 | });
174 | });
175 |
176 | TestRouter::debug('/test', 'get');
177 |
178 | $this->assertFalse($result);
179 |
180 | }
181 |
182 | public function testRegEx()
183 | {
184 | TestRouter::get('/my/{path}', 'DummyController@method1')->where(['path' => '[a-zA-Z-]+']);
185 | TestRouter::debug('/my/custom-path', 'get');
186 |
187 | $this->assertTrue(true);
188 | }
189 |
190 | public function testParametersWithDashes()
191 | {
192 |
193 | $defaultVariable = null;
194 |
195 | TestRouter::get('/my/{path}', function ($path = 'working') use (&$defaultVariable) {
196 | $defaultVariable = $path;
197 | });
198 |
199 | TestRouter::debug('/my/hello-motto-man');
200 |
201 | $this->assertEquals('hello-motto-man', $defaultVariable);
202 |
203 | }
204 |
205 | public function testParameterDefaultValue()
206 | {
207 |
208 | $defaultVariable = null;
209 |
210 | TestRouter::get('/my/{path?}', function ($path = 'working') use (&$defaultVariable) {
211 | $defaultVariable = $path;
212 | });
213 |
214 | TestRouter::debug('/my/');
215 |
216 | $this->assertEquals('working', $defaultVariable);
217 |
218 | }
219 |
220 | public function testDefaultParameterRegex()
221 | {
222 | TestRouter::get('/my/{path}', 'DummyController@param', ['defaultParameterRegex' => '[\w-]+']);
223 | $output = TestRouter::debugOutput('/my/custom-regex', 'get');
224 |
225 | $this->assertEquals('custom-regex', $output);
226 | }
227 |
228 | public function testDefaultParameterRegexGroup()
229 | {
230 | TestRouter::group(['defaultParameterRegex' => '[\w-]+'], function () {
231 | TestRouter::get('/my/{path}', 'DummyController@param');
232 | });
233 |
234 | $output = TestRouter::debugOutput('/my/custom-regex', 'get');
235 |
236 | $this->assertEquals('custom-regex', $output);
237 | }
238 |
239 | public function testClassHint()
240 | {
241 | TestRouter::get('/my/test/url', ['DummyController', 'method1']);
242 | TestRouter::all('/my/test/url', ['DummyController', 'method1']);
243 | TestRouter::match(['put', 'get', 'post'], '/my/test/url', ['DummyController', 'method1']);
244 |
245 | TestRouter::debug('/my/test/url', 'get');
246 |
247 | $this->assertTrue(true);
248 | }
249 |
250 | public function testDefaultNameSpaceOverload()
251 | {
252 | TestRouter::setDefaultNamespace('DefaultNamespace\\Controllers');
253 | TestRouter::get('/test', [\MyNamespace\NSController::class, 'method']);
254 |
255 | $result = TestRouter::debugOutput('/test');
256 |
257 | $this->assertTrue( (bool)$result);
258 | }
259 |
260 | public function testSameRoutes()
261 | {
262 | TestRouter::get('/recipe', 'DummyController@method1')->name('add');
263 | TestRouter::post('/recipe', 'DummyController@method2')->name('edit');
264 |
265 | TestRouter::debugNoReset('/recipe', 'post');
266 | TestRouter::debug('/recipe', 'get');
267 |
268 | $this->assertTrue(true);
269 | }
270 |
271 | }
--------------------------------------------------------------------------------
/src/Pecee/SimpleRouter/Route/LoadableRoute.php:
--------------------------------------------------------------------------------
1 | debug('Loading middlewares');
37 |
38 | foreach ($this->getMiddlewares() as $middleware) {
39 |
40 | if (is_object($middleware) === false) {
41 | $middleware = $router->getClassLoader()->loadClass($middleware);
42 | }
43 |
44 | if (($middleware instanceof IMiddleware) === false) {
45 | throw new HttpException($middleware . ' must be inherit the IMiddleware interface');
46 | }
47 |
48 | $className = get_class($middleware);
49 |
50 | $router->debug('Loading middleware "%s"', $className);
51 | $middleware->handle($request);
52 | $router->debug('Finished loading middleware "%s"', $className);
53 | }
54 |
55 | $router->debug('Finished loading middlewares');
56 | }
57 |
58 | /**
59 | * @param Request $request
60 | * @param string $url
61 | * @return bool|null
62 | */
63 | public function matchRegex(Request $request, string $url): ?bool
64 | {
65 | /* Match on custom defined regular expression */
66 | if ($this->regex === null) {
67 | return null;
68 | }
69 |
70 | $parameters = [];
71 | if ((bool)preg_match($this->regex, $url, $parameters) !== false) {
72 | $this->setParameters($parameters);
73 |
74 | return true;
75 | }
76 |
77 | return false;
78 | }
79 |
80 | /**
81 | * Set url
82 | *
83 | * @param string $url
84 | * @return static
85 | */
86 | public function setUrl(string $url): ILoadableRoute
87 | {
88 | $this->url = ($url === '/') ? '/' : '/' . trim($url, '/') . '/';
89 |
90 | if (str_contains($this->url, $this->paramModifiers[0])) {
91 |
92 | $regex = sprintf(static::PARAMETERS_REGEX_FORMAT, $this->paramModifiers[0], $this->paramOptionalSymbol, $this->paramModifiers[1]);
93 |
94 | if ((bool)preg_match_all('/' . $regex . '/u', $this->url, $matches) !== false) {
95 | $this->parameters = array_fill_keys($matches[1], null);
96 | }
97 | }
98 |
99 | return $this;
100 | }
101 |
102 | /**
103 | * Prepends url while ensuring that the url has the correct formatting.
104 | *
105 | * @param string $url
106 | * @return ILoadableRoute
107 | */
108 | public function prependUrl(string $url): ILoadableRoute
109 | {
110 | return $this->setUrl(rtrim($url, '/') . $this->url);
111 | }
112 |
113 | public function getUrl(): string
114 | {
115 | return $this->url;
116 | }
117 |
118 | /**
119 | * Returns true if group is defined and matches the given url.
120 | *
121 | * @param string $url
122 | * @param Request $request
123 | * @return bool
124 | */
125 | protected function matchGroup(string $url, Request $request): bool
126 | {
127 | return ($this->getGroup() === null || $this->getGroup()->matchRoute($url, $request) === true);
128 | }
129 |
130 | /**
131 | * Find url that matches method, parameters or name.
132 | * Used when calling the url() helper.
133 | *
134 | * @param string|null $method
135 | * @param array|string|null $parameters
136 | * @param string|null $name
137 | * @return string
138 | */
139 | public function findUrl(?string $method = null, array|string $parameters = null, ?string $name = null): string
140 | {
141 | $url = $this->getUrl();
142 |
143 | $group = $this->getGroup();
144 |
145 | if ($group !== null && count($group->getDomains()) !== 0) {
146 | $url = '//' . $group->getDomains()[0] . $url;
147 | }
148 |
149 | /* Create the param string - {parameter} */
150 | $param1 = $this->paramModifiers[0] . '%s' . $this->paramModifiers[1];
151 |
152 | /* Create the param string with the optional symbol - {parameter?} */
153 | $param2 = $this->paramModifiers[0] . '%s' . $this->paramOptionalSymbol . $this->paramModifiers[1];
154 |
155 | /* Replace any {parameter} in the url with the correct value */
156 |
157 | $params = $this->getParameters();
158 |
159 | foreach (array_keys($params) as $param) {
160 |
161 | if ($parameters === '' || (is_array($parameters) === true && count($parameters) === 0)) {
162 | $value = '';
163 | } else {
164 | $p = (array)$parameters;
165 | $value = array_key_exists($param, $p) ? $p[$param] : $params[$param];
166 |
167 | /* If parameter is specifically set to null - use the original-defined value */
168 | if ($value === null && isset($this->originalParameters[$param]) === true) {
169 | $value = $this->originalParameters[$param];
170 | }
171 | }
172 |
173 | if (stripos($url, $param1) !== false || stripos($url, $param) !== false) {
174 | /* Add parameter to the correct position */
175 | $url = str_ireplace([sprintf($param1, $param), sprintf($param2, $param)], $value ?? '', $url);
176 | } else {
177 | /* Parameter aren't recognized and will be appended at the end of the url */
178 | $url .= $value . '/';
179 | }
180 | }
181 |
182 | return rtrim('/' . ltrim($url, '/'), '/') . '/';
183 | }
184 |
185 | /**
186 | * Returns the provided name for the router.
187 | *
188 | * @return string|null
189 | */
190 | public function getName(): ?string
191 | {
192 | return $this->name;
193 | }
194 |
195 | /**
196 | * Check if route has given name.
197 | *
198 | * @param string $name
199 | * @return bool
200 | */
201 | public function hasName(string $name): bool
202 | {
203 | if($this->getName() === null)
204 | return false;
205 | return strtolower($this->getName()) === strtolower($name);
206 | }
207 |
208 | /**
209 | * Add regular expression match for the entire route.
210 | *
211 | * @param string $regex
212 | * @return static
213 | */
214 | public function setMatch(string $regex): ILoadableRoute
215 | {
216 | $this->regex = $regex;
217 |
218 | return $this;
219 | }
220 |
221 | /**
222 | * Get regular expression match used for matching route (if defined).
223 | *
224 | * @return string
225 | */
226 | public function getMatch(): string
227 | {
228 | return $this->regex;
229 | }
230 |
231 | /**
232 | * Sets the router name, which makes it easier to obtain the url or router at a later point.
233 | * Alias for LoadableRoute::setName().
234 | *
235 | * @param string|array $name
236 | * @return static
237 | * @see LoadableRoute::setName()
238 | */
239 | public function name(array|string $name): ILoadableRoute
240 | {
241 | return $this->setName($name);
242 | }
243 |
244 | /**
245 | * Sets the router name, which makes it easier to obtain the url or router at a later point.
246 | *
247 | * @param string $name
248 | * @return static
249 | */
250 | public function setName(string $name): ILoadableRoute
251 | {
252 | $this->name = $name;
253 |
254 | return $this;
255 | }
256 |
257 | /**
258 | * Merge with information from another route.
259 | *
260 | * @param array $settings
261 | * @param bool $merge
262 | * @return static
263 | */
264 | public function setSettings(array $settings, bool $merge = false): IRoute
265 | {
266 | if (isset($settings['as']) === true) {
267 |
268 | $name = $settings['as'];
269 |
270 | if ($this->name !== null && $merge !== false) {
271 | $name .= '.' . $this->name;
272 | }
273 |
274 | $this->setName($name);
275 | }
276 |
277 | if (isset($settings['prefix']) === true) {
278 | $this->prependUrl($settings['prefix']);
279 | }
280 |
281 | return parent::setSettings($settings, $merge);
282 | }
283 |
284 | }
--------------------------------------------------------------------------------
/src/Pecee/Http/Input/InputFile.php:
--------------------------------------------------------------------------------
1 | index = $index;
59 |
60 | $this->errors = 0;
61 |
62 | // Make the name human friendly, by replace _ with space
63 | $this->name = ucfirst(str_replace('_', ' ', strtolower($this->index)));
64 | }
65 |
66 | /**
67 | * Create from array
68 | *
69 | * @param array $values
70 | * @throws InvalidArgumentException
71 | * @return static
72 | */
73 | public static function createFromArray(array $values): self
74 | {
75 | if (isset($values['index']) === false) {
76 | throw new InvalidArgumentException('Index key is required');
77 | }
78 |
79 | /* Easy way of ensuring that all indexes-are set and not filling the screen with isset() */
80 |
81 | $values += [
82 | 'tmp_name' => null,
83 | 'type' => null,
84 | 'size' => null,
85 | 'name' => null,
86 | 'error' => null,
87 | ];
88 |
89 | return (new static($values['index']))
90 | ->setSize((int)$values['size'])
91 | ->setError((int)$values['error'])
92 | ->setType($values['type'])
93 | ->setTmpName($values['tmp_name'])
94 | ->setFilename($values['name']);
95 |
96 | }
97 |
98 | /**
99 | * @return string
100 | */
101 | public function getIndex(): string
102 | {
103 | return $this->index;
104 | }
105 |
106 | /**
107 | * Set input index
108 | * @param string $index
109 | * @return static
110 | */
111 | public function setIndex(string $index): IInputItem
112 | {
113 | $this->index = $index;
114 |
115 | return $this;
116 | }
117 |
118 | /**
119 | * @return int|null
120 | */
121 | public function getSize(): ?int
122 | {
123 | return $this->size;
124 | }
125 |
126 | /**
127 | * Set file size
128 | * @param int $size
129 | * @return static
130 | */
131 | public function setSize(int $size): IInputItem
132 | {
133 | $this->size = $size;
134 |
135 | return $this;
136 | }
137 |
138 | /**
139 | * Get mime-type of file
140 | * @return string
141 | */
142 | public function getMime(): string
143 | {
144 | return $this->getType();
145 | }
146 |
147 | /**
148 | * @return string
149 | */
150 | public function getType(): string
151 | {
152 | return $this->type;
153 | }
154 |
155 | /**
156 | * Set type
157 | * @param string $type
158 | * @return static
159 | */
160 | public function setType(string $type): IInputItem
161 | {
162 | $this->type = $type;
163 |
164 | return $this;
165 | }
166 |
167 | /**
168 | * Returns extension without "."
169 | *
170 | * @return string|null
171 | */
172 | public function getExtension(): ?string
173 | {
174 | if($this->getFilename() === null)
175 | return null;
176 | return pathinfo($this->getFilename(), PATHINFO_EXTENSION);
177 | }
178 |
179 | /**
180 | * Get human friendly name
181 | *
182 | * @return string
183 | */
184 | public function getName(): ?string
185 | {
186 | return $this->name;
187 | }
188 |
189 | /**
190 | * Set human friendly name.
191 | * Useful for adding validation etc.
192 | *
193 | * @param string $name
194 | * @return static
195 | */
196 | public function setName(string $name): IInputItem
197 | {
198 | $this->name = $name;
199 |
200 | return $this;
201 | }
202 |
203 | /**
204 | * Set filename
205 | *
206 | * @param string $name
207 | * @return static
208 | */
209 | public function setFilename(string $name): IInputItem
210 | {
211 | $this->filename = $name;
212 |
213 | return $this;
214 | }
215 |
216 | /**
217 | * Get filename
218 | *
219 | * @return string|null
220 | */
221 | public function getFilename(): ?string
222 | {
223 | return $this->filename;
224 | }
225 |
226 | /**
227 | * Move the uploaded temporary file to it's new home
228 | *
229 | * @param string $destination
230 | * @return bool
231 | */
232 | public function move(string $destination): bool
233 | {
234 | return move_uploaded_file($this->tmpName, $destination);
235 | }
236 |
237 | /**
238 | * Get file contents
239 | *
240 | * @return string
241 | */
242 | public function getContents(): string
243 | {
244 | return file_get_contents($this->tmpName);
245 | }
246 |
247 | /**
248 | * Return true if an upload error occurred.
249 | *
250 | * @return bool
251 | */
252 | public function hasError(): bool
253 | {
254 | return ($this->getError() !== 0);
255 | }
256 |
257 | /**
258 | * Get upload-error code.
259 | *
260 | * @return int|null
261 | */
262 | public function getError(): ?int
263 | {
264 | return $this->errors;
265 | }
266 |
267 | /**
268 | * Set error
269 | *
270 | * @param int|null $error
271 | * @return static
272 | */
273 | public function setError(?int $error): IInputItem
274 | {
275 | $this->errors = (int)$error;
276 |
277 | return $this;
278 | }
279 |
280 | /**
281 | * @return string|null
282 | */
283 | public function getTmpName(): ?string
284 | {
285 | return $this->tmpName;
286 | }
287 |
288 | /**
289 | * Set file temp. name
290 | * @param string $name
291 | * @return static
292 | */
293 | public function setTmpName(string $name): IInputItem
294 | {
295 | $this->tmpName = $name;
296 |
297 | return $this;
298 | }
299 |
300 | public function __toString(): string
301 | {
302 | return $this->getTmpName();
303 | }
304 |
305 | /**
306 | * @return string|null
307 | */
308 | public function getValue(): ?string
309 | {
310 | return $this->getFilename();
311 | }
312 |
313 | /**
314 | * @param mixed $value
315 | * @return static
316 | */
317 | public function setValue(mixed $value): IInputItem
318 | {
319 | $this->filename = $value;
320 |
321 | return $this;
322 | }
323 |
324 | /**
325 | * @return bool
326 | */
327 | public function hasInputItems(): bool
328 | {
329 | return is_array($this->value);
330 | }
331 |
332 | /**
333 | * @return InputFile[]
334 | */
335 | public function getInputItems(): array
336 | {
337 | if($this->hasInputItems()){
338 | return $this->value;
339 | }
340 | return array();
341 | }
342 |
343 | /**
344 | * @param array|InputFile $inputFile
345 | * @return static
346 | */
347 | public function addInputFile(InputFile|array $inputFile): IInputItem
348 | {
349 | if(!is_array($this->value)){
350 | $this->value = array();
351 | }
352 | if(is_array($inputFile)){
353 | $this->value = array_merge($this->value, $inputFile);
354 | }else{
355 | $this->value[] = $inputFile;
356 | }
357 |
358 | return $this;
359 | }
360 |
361 | /**
362 | * @param string|array $rules
363 | * @return Validation
364 | * @throws InputValidationException
365 | */
366 | public function validate(string|array $rules): Validation{
367 | return InputValidator::make()->validateItem($this, $rules);
368 | }
369 |
370 | public function toArray(): array
371 | {
372 | return [
373 | 'tmp_name' => $this->tmpName,
374 | 'type' => $this->type,
375 | 'size' => $this->size,
376 | 'name' => $this->name,
377 | 'error' => $this->errors,
378 | 'filename' => $this->filename,
379 | ];
380 | }
381 |
382 | public function getIterator(): ArrayIterator
383 | {
384 | return new ArrayIterator($this->getInputItems());
385 | }
386 |
387 | }
--------------------------------------------------------------------------------
/src/Pecee/Http/Input/InputValidator.php:
--------------------------------------------------------------------------------
1 | rules = array();
90 | }
91 |
92 | /**
93 | * @param string $key
94 | * @param string $validation
95 | * @return self
96 | */
97 | public function addRule(string $key, string $validation): self
98 | {
99 | $this->rules[$key] = $validation;
100 | return $this;
101 | }
102 |
103 | /**
104 | * @param array $rules
105 | * @return self
106 | */
107 | public function setRules(array $rules): self
108 | {
109 | $this->rules = $rules;
110 | return $this;
111 | }
112 |
113 | /**
114 | * @return array
115 | */
116 | public function getRules(): array
117 | {
118 | return $this->rules;
119 | }
120 |
121 | /**
122 | * @param string|Closure $callback
123 | * @return self
124 | */
125 | protected function rewriteCallbackOnFailure(string|Closure $callback): self
126 | {
127 | $this->rewriteCallbackOnFailure = $callback;
128 | return $this;
129 | }
130 |
131 | /**
132 | * @param InputHandler $inputHandler
133 | * @return Validation
134 | * @throws InputValidationException
135 | */
136 | public function validateInputs(InputHandler $inputHandler): Validation
137 | {
138 | return $this->validateItems($inputHandler);
139 | }
140 |
141 | /**
142 | * @param Request $request
143 | * @return Validation
144 | * @throws InputValidationException
145 | */
146 | public function validateRequest(Request $request): Validation
147 | {
148 | return $this->validateItems($request->getInputHandler());
149 | }
150 |
151 | /**
152 | * @param InputHandler $inputHandler
153 | * @return Validation
154 | * @throws InputValidationException
155 | */
156 | private function validateItems(InputHandler $inputHandler): Validation
157 | {
158 | $validation = self::getFactory()->validate($inputHandler->values(array_keys($this->getRules())), $this->getRules());
159 | if ($validation->fails() && self::isThrowExceptions()) {
160 | throw new InputValidationException('Failed to validate inputs', $validation);
161 | }
162 | return $validation;
163 | }
164 |
165 | /**
166 | * @param InputItem|InputFile $inputItem
167 | * @param string|array $rules
168 | * @return Validation
169 | * @throws InputValidationException
170 | */
171 | public function validateItem(InputItem|InputFile $inputItem, string|array $rules): Validation
172 | {
173 | $validation = self::getFactory()->validate(array(
174 | $inputItem->getName() => $inputItem->getValue()
175 | ), array(
176 | $inputItem->getName() => $rules
177 | ));
178 | if ($validation->fails() && self::isThrowExceptions()) {
179 | throw new InputValidationException('Failed to validate input', $validation);
180 | }
181 | return $validation;
182 | }
183 |
184 | /**
185 | * @param array $data
186 | * @return Validation
187 | * @throws InputValidationException
188 | */
189 | public function validateData(array $data): Validation
190 | {
191 | $validation = self::getFactory()->validate($data, $this->getRules());
192 | if ($validation->fails() && self::isThrowExceptions()) {
193 | throw new InputValidationException('Failed to validate inputs', $validation);
194 | }
195 | return $validation;
196 | }
197 |
198 | /**
199 | * @param Router $router
200 | * @param IRoute $route
201 | * @return InputValidator|null
202 | * @since 8.0
203 | */
204 | public static function parseValidatorFromRoute(Router $router, IRoute $route): ?InputValidator
205 | {
206 | $routeAttributeValidator = null;
207 | if(InputValidator::$parseAttributes){
208 | $reflectionMethod = self::getReflection($router, $route);
209 | if($reflectionMethod !== null){
210 | $attributes = $reflectionMethod->getAttributes(ValidatorAttribute::class);
211 | if(sizeof($attributes) > 0){
212 | $settings = array();
213 | foreach($attributes as $attribute){
214 | /* @var ValidatorAttribute $routeAttribute */
215 | $routeAttribute = $attribute->newInstance();
216 | if($routeAttribute->getName() !== null)
217 | $settings[$routeAttribute->getName()] = $routeAttribute->getFullValidator();
218 | }
219 | $routeAttributeValidator = InputValidator::make()->setRules($settings);
220 | }
221 | }
222 | }
223 | return $routeAttributeValidator;
224 | }
225 |
226 | /**
227 | * @param Router $router
228 | * @param IRoute $route
229 | * @return InputValidator|null
230 | * @since 8.0
231 | */
232 | public static function parseValidatorFromRouteParameters(Router $router, IRoute $route): ?InputValidator
233 | {
234 | $parsedData = $route->getParameters();
235 | $routeAttributeValidator = null;
236 | if(InputValidator::$parseAttributes){
237 | $reflectionMethod = self::getReflection($router, $route);
238 | if($reflectionMethod !== null){
239 | $parameters = $reflectionMethod->getParameters();
240 | if(sizeof($parameters) > 0){
241 | $settings = array();
242 | foreach($parameters as $parameter){
243 | $attributes = $parameter->getAttributes(ValidatorAttribute::class);
244 | if(sizeof($attributes) > 0){
245 | /* @var ValidatorAttribute $routeAttribute */
246 | $routeAttribute = $attributes[0]->newInstance();
247 | if($routeAttribute->getName() === null)
248 | $routeAttribute->setName($parameter->getName());
249 | if($routeAttribute->getType() === null && $parameter->getType() !== null){
250 | $routeAttribute->setType($parameter->getType()->getName());
251 | if($parameter->getType()->allowsNull())
252 | $routeAttribute->addValidator('nullable');
253 | }
254 | $settings[$routeAttribute->getName()] = $routeAttribute->getFullValidator();
255 | $parsedData[$routeAttribute->getName()] = $routeAttribute->getType() !== null ? (new InputParser(new InputItem($routeAttribute->getName(), $parsedData[$routeAttribute->getName()] ?? null)))->parseFromSetting($routeAttribute->getType())->getValue() : $parsedData[$routeAttribute->getName()] ?? null;
256 | }
257 | }
258 | $routeAttributeValidator = InputValidator::make()->setRules($settings);
259 | }
260 | }
261 | }
262 | $route->setOriginalParameters($parsedData);
263 | return $routeAttributeValidator;
264 | }
265 |
266 | /**
267 | * @param Router $router
268 | * @param IRoute|null $route
269 | * @return ReflectionFunction|ReflectionMethod|null
270 | */
271 | public static function getReflection(Router $router, ?IRoute $route = null): ReflectionMethod|ReflectionFunction|null
272 | {
273 | $reflectionMethod = null;
274 | if($route === null){
275 | $route = SimpleRouter::router()->getCurrentProcessingRoute();
276 | }
277 | if($route === null){
278 | $route = SimpleRouter::request()->getLoadedRoute();
279 | }
280 | $callback = $route->getCallback();
281 | try{
282 | if($callback !== null){
283 | if(is_callable($callback) === true){
284 | /* Load class from type hinting */
285 | if(is_array($callback) === true && isset($callback[0], $callback[1]) === true){
286 | $callback[0] = $router->getClassLoader()->loadClass($callback[0]);
287 | }
288 |
289 | /* When the callback is a function */
290 | $reflectionMethod = is_array($callback) ? new ReflectionMethod($callback[0], $callback[1]) : ($callback instanceof Closure ? new ReflectionFunction($callback) : new ReflectionMethod($callback));
291 | } else {
292 |
293 | $controller = $route->getClass();
294 | $method = $route->getMethod();
295 |
296 | $namespace = $route->getNamespace();
297 | $className = ($namespace !== null && $controller[0] !== '\\') ? $namespace . '\\' . $controller : $controller;
298 | $class = $router->getClassLoader()->loadClass($className);
299 |
300 | if($method === null){
301 | $method = '__invoke';
302 | }
303 |
304 | if(method_exists($class, $method) !== false){
305 | $reflectionMethod = new ReflectionMethod($class, $method);
306 | }
307 | }
308 | }
309 | }catch(ReflectionException $e){}
310 | return $reflectionMethod;
311 | }
312 | }
--------------------------------------------------------------------------------
/tests/Pecee/SimpleRouter/InputHandlerTest.php:
--------------------------------------------------------------------------------
1 | 'Pepsi',
29 | 1 => 'Coca Cola',
30 | 2 => 'Harboe',
31 | 3 => 'Mountain Dew',
32 | ];
33 |
34 | protected $day = 'monday';
35 |
36 | public function testPost()
37 | {
38 | global $_POST;
39 |
40 | $_POST = [
41 | 'names' => $this->names,
42 | 'day' => $this->day,
43 | 'sodas' => $this->sodas,
44 | 'assoc' => [
45 | array(
46 | 'test' => 'data',
47 | 'assoc4' => array(
48 | 'id' => 1
49 | )
50 | ),
51 | array(
52 | 'test' => 'data2',
53 | 'assoc4' => array(
54 | 'id' => 1
55 | )
56 | )
57 | ],
58 | 'assoc2' => array(
59 | 'assoc3' => array(
60 | 'id' => 1
61 | )
62 | )
63 | ];
64 | $request = new Request(false);
65 |
66 | $request->setMethod('post');
67 | TestRouter::setRequest($request);
68 |
69 | $handler = TestRouter::request()->getInputHandler();
70 |
71 | $this->assertEquals($this->names, $handler->value('names'));
72 | $this->assertEquals($this->names, $handler->all(['names'])['names']->getValue());
73 | $this->assertEquals($this->day, $handler->value('day'));
74 | $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $handler->find('day'));
75 | $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $handler->post('day'));
76 | $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $handler->find('day', null, 'post'));
77 |
78 | // Check non-existing and wrong request-type
79 | $this->assertCount(1, $handler->all(['non-existing']));
80 | $this->assertEmpty($handler->all(['non-existing'])['non-existing']->getValue());
81 | $this->assertNull($handler->value('non-existing'));
82 | $this->assertNull($handler->find('non-existing')->getValue());
83 | $this->assertNull($handler->value('names', null, 'get'));
84 | $this->assertNull($handler->find('names', null, 'get')->getValue());
85 | $this->assertEquals($this->sodas, $handler->value('sodas'));
86 |
87 | //Nested
88 | $this->assertCount(1, $handler->all(['test.non-existing']));
89 | //TODO fix nested filters
90 | //$this->assertCount(3, $handler->values(['assoc.*.test', 'assoc.*.test2'])['assoc'][0]);
91 | //$this->assertCount(3, $handler->values(['assoc.*.assoc4.id', 'assoc.*.assoc4.test', 'assoc.*.test2'])['assoc'][0]);
92 | //$this->assertCount(2, $handler->values(['assoc.*.assoc4.id', 'assoc.*.assoc4.test', 'assoc.*.test2'])['assoc'][0]['assoc4']);
93 | $this->assertCount(2, $handler->values(['assoc2.assoc3.id', 'assoc2.assoc3.test'])['assoc2']['assoc3']);
94 |
95 | $objects = $handler->find('names');
96 |
97 | $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $objects);
98 | $this->assertCount(4, $objects);
99 |
100 | /* @var $object \Pecee\Http\Input\InputItem */
101 | foreach($objects->getInputItems() as $i => $object) {
102 | $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $object);
103 | $this->assertEquals($this->names[$i], $object->getValue());
104 | }
105 |
106 | // Reset
107 | $_POST = [];
108 | }
109 |
110 | public function testGet()
111 | {
112 | global $_GET;
113 |
114 | $_GET = [
115 | 'names' => $this->names,
116 | 'day' => $this->day,
117 | ];
118 |
119 | $request = new Request(false);
120 | $request->setMethod('get');
121 | TestRouter::setRequest($request);
122 |
123 | $handler = TestRouter::request()->getInputHandler();
124 |
125 | $this->assertEquals($this->names, $handler->value('names'));
126 | $this->assertEquals($this->names, $handler->all(['names'])['names']->getValue());
127 | $this->assertEquals($this->day, $handler->value('day'));
128 | $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $handler->find('day'));
129 | $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $handler->get('day'));
130 |
131 | // Check non-existing and wrong request-type
132 | $this->assertCount(1, $handler->all(['non-existing']));
133 | $this->assertEmpty($handler->all(['non-existing'])['non-existing']->getValue());
134 | $this->assertNull($handler->value('non-existing'));
135 | $this->assertNull($handler->find('non-existing')->getValue());
136 | $this->assertNull($handler->value('names', null, 'post'));
137 | $this->assertNull($handler->find('names', null, 'post')->getValue());
138 |
139 | $objects = $handler->find('names');
140 |
141 | $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $objects);
142 | $this->assertCount(4, $objects);
143 |
144 | /* @var $object \Pecee\Http\Input\InputItem */
145 | foreach($objects->getInputItems() as $i => $object) {
146 | $this->assertInstanceOf(\Pecee\Http\Input\InputItem::class, $object);
147 | $this->assertEquals($this->names[$i], $object->getValue());
148 | }
149 |
150 | // Reset
151 | $_GET = [];
152 | }
153 |
154 | public function testFindInput() {
155 |
156 | global $_POST;
157 | $_POST['hello'] = 'motto';
158 |
159 | $request = new Request(false);
160 | $request->setMethod('post');
161 | TestRouter::setRequest($request);
162 |
163 | $inputHandler = TestRouter::request()->getInputHandler();
164 |
165 | $value = $inputHandler->value('hello', null, Request::$requestTypesPost);
166 |
167 | $this->assertEquals($_POST['hello'], $value);
168 | }
169 |
170 | public function testFile()
171 | {
172 | global $_FILES;
173 |
174 | $testFile = $this->generateFile();
175 |
176 | $_FILES = [
177 | 'test_input' => $testFile,
178 | ];
179 |
180 | $request = new Request(false);
181 | $request->setMethod('post');
182 | TestRouter::setRequest($request);
183 |
184 | $inputHandler = TestRouter::request()->getInputHandler();
185 |
186 | $testFileContent = md5(uniqid('test', false));
187 |
188 | $file = $inputHandler->file('test_input');
189 |
190 | $this->assertInstanceOf(InputFile::class, $file);
191 | $this->assertEquals($testFile['name'], $file->getFilename());
192 | $this->assertEquals($testFile['type'], $file->getType());
193 | $this->assertEquals($testFile['tmp_name'], $file->getTmpName());
194 | $this->assertEquals($testFile['error'], $file->getError());
195 | $this->assertEquals($testFile['size'], $file->getSize());
196 | $this->assertEquals(pathinfo($testFile['name'], PATHINFO_EXTENSION), $file->getExtension());
197 |
198 | file_put_contents($testFile['tmp_name'], $testFileContent);
199 | $this->assertEquals($testFileContent, $file->getContents());
200 |
201 | // Cleanup
202 | unlink($testFile['tmp_name']);
203 | }
204 |
205 | public function testFilesArray()
206 | {
207 | global $_FILES;
208 |
209 | $testFiles = [
210 | $file = $this->generateFile(),
211 | $file = $this->generateFile(),
212 | $file = $this->generateFile(),
213 | $file = $this->generateFile(),
214 | $file = $this->generateFile(),
215 | ];
216 |
217 | $_FILES = [
218 | 'my_files' => $testFiles,
219 | ];
220 |
221 | $request = new Request(false);
222 | $request->setMethod('post');
223 | TestRouter::setRequest($request);
224 |
225 | $inputHandler = TestRouter::request()->getInputHandler();
226 |
227 | $files = $inputHandler->file('my_files');
228 | $this->assertCount(5, $files->getInputItems());
229 |
230 | /* @var $file InputFile */
231 | foreach ($files as $key => $file) {
232 |
233 | $testFileContent = md5(uniqid('test', false));
234 |
235 | $this->assertInstanceOf(InputFile::class, $file);
236 | $this->assertEquals($testFiles[$key]['name'], $file->getFilename());
237 | $this->assertEquals($testFiles[$key]['type'], $file->getType());
238 | $this->assertEquals($testFiles[$key]['tmp_name'], $file->getTmpName());
239 | $this->assertEquals($testFiles[$key]['error'], $file->getError());
240 | $this->assertEquals($testFiles[$key]['size'], $file->getSize());
241 | $this->assertEquals(pathinfo($testFiles[$key]['name'], PATHINFO_EXTENSION), $file->getExtension());
242 |
243 | file_put_contents($testFiles[$key]['tmp_name'], $testFileContent);
244 |
245 | $this->assertEquals($testFileContent, $file->getContents());
246 |
247 | // Cleanup
248 | unlink($testFiles[$key]['tmp_name']);
249 | }
250 |
251 | }
252 |
253 | public function testAll()
254 | {
255 | global $_POST;
256 | global $_GET;
257 |
258 | $_POST = [
259 | 'names' => $this->names,
260 | 'is_sad' => true,
261 | ];
262 |
263 | $_GET = [
264 | 'brands' => $this->brands,
265 | 'is_happy' => true,
266 | ];
267 |
268 | $request = new Request(false);
269 | $request->setMethod('post');
270 | TestRouter::setRequest($request);
271 |
272 | $handler = TestRouter::request()->getInputHandler();
273 |
274 | // GET
275 | $brandsFound = $handler->values(['brands', 'nothing']);
276 |
277 | $this->assertArrayHasKey('brands', $brandsFound);
278 | $this->assertArrayHasKey('nothing', $brandsFound);
279 | $this->assertEquals($this->brands, $brandsFound['brands']);
280 | $this->assertNull($brandsFound['nothing']);
281 |
282 | // POST
283 | $namesFound = $handler->values(['names', 'nothing']);
284 |
285 | $this->assertArrayHasKey('names', $namesFound);
286 | $this->assertArrayHasKey('nothing', $namesFound);
287 | $this->assertEquals($this->names, $namesFound['names']);
288 | $this->assertNull($namesFound['nothing']);
289 |
290 | // DEFAULT VALUE
291 | $nonExisting = $handler->values([
292 | 'non-existing'
293 | ]);
294 |
295 | $this->assertArrayHasKey('non-existing', $nonExisting);
296 | $this->assertNull($nonExisting['non-existing']);
297 |
298 | // Reset
299 | $_GET = [];
300 | $_POST = [];
301 | }
302 |
303 | public function testAllNested()
304 | {
305 | TestRouter::resetRouter();
306 | global $_POST;
307 |
308 | $_POST = [
309 | 'data' => array(
310 | 'id' => "1",
311 | 'name' => 'Max',
312 | 'company' => ''
313 | )
314 | ];
315 |
316 | $request = new Request(false);
317 | $request->setMethod('post');
318 | TestRouter::setRequest($request);
319 |
320 | TestRouter::post('/test', 'DummyController@method5');
321 |
322 | $output = TestRouter::debugOutput('/test', 'post');
323 |
324 | $this->assertEquals(json_encode(array(
325 | 'data' => array(
326 | 'id' => 1,
327 | 'name' => 'Max',
328 | 'company' => null
329 | )
330 | )), $output);
331 | }
332 |
333 | public function testAllNestedValidation()
334 | {
335 | TestRouter::resetRouter();
336 | global $_POST;
337 |
338 | $_POST = [
339 | 'data' => array(
340 | 'id' => "1"
341 | )
342 | ];
343 |
344 | $request = new Request(false);
345 | $request->setMethod('post');
346 | TestRouter::setRequest($request);
347 | InputValidator::$parseAttributes = true;
348 |
349 | $this->expectException(\Pecee\Http\Input\Exceptions\InputValidationException::class);
350 |
351 | TestRouter::post('/test', 'DummyController@method5');
352 |
353 | TestRouter::debug('/test', 'post');
354 | }
355 |
356 | protected function generateFile()
357 | {
358 | return [
359 | 'name' => uniqid('', false) . '.txt',
360 | 'type' => 'text/plain',
361 | 'tmp_name' => sys_get_temp_dir() . '/phpYfWUiw',
362 | 'error' => 0,
363 | 'size' => rand(3, 40),
364 | ];
365 | }
366 |
367 | protected function generateFileContent()
368 | {
369 | return md5(uniqid('', false));
370 | }
371 |
372 | }
--------------------------------------------------------------------------------