├── .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 | } --------------------------------------------------------------------------------