Skip to content

Commit

Permalink
Merge pull request #529 from getformwork/feature/handle-site-post-data
Browse files Browse the repository at this point in the history
Handle POST requests to site pages
  • Loading branch information
giuscris authored Jun 23, 2024
2 parents 9f8fb4c + 6f728c3 commit 3edb85e
Show file tree
Hide file tree
Showing 10 changed files with 108 additions and 31 deletions.
51 changes: 47 additions & 4 deletions formwork/routes.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
<?php

use Formwork\Config\Config;
use Formwork\ErrorHandlers;
use Formwork\Http\RedirectResponse;
use Formwork\Http\Request;
use Formwork\Http\ResponseStatus;
use Formwork\Languages\Languages;
use Formwork\Router\Router;
use Formwork\Security\CsrfToken;
use Formwork\Utils\FileSystem;
use Formwork\Utils\Str;

return [
'routes' => [
'index' => [
'path' => '/',
'action' => 'Formwork\Controllers\PageController@load',
'path' => '/',
'action' => 'Formwork\Controllers\PageController@load',
'methods' => ['GET', 'POST'],
],
'index.pagination' => [
'path' => '/page/{paginationPage:num}/',
Expand All @@ -34,12 +39,50 @@
'action' => 'Formwork\Controllers\PageController@load',
],
'page' => [
'path' => '/{page}/',
'action' => 'Formwork\Controllers\PageController@load',
'path' => '/{page}/',
'action' => 'Formwork\Controllers\PageController@load',
'methods' => ['GET', 'POST'],
],
],

'filters' => [
'request.validateSize' => [
'action' => static function (Config $config, Request $request, Router $router, ErrorHandlers $errorHandlers) {
if ($config->get('system.panel.enabled') && $router->requestHasPrefix($config->get('system.panel.root'))) {
return;
}

// Validate HTTP request Content-Length according to `post_max_size` directive
if ($request->contentLength() !== null) {
$maxSize = FileSystem::shorthandToBytes(ini_get('post_max_size') ?: '0');

if ($request->contentLength() > $maxSize && $maxSize > 0) {
$errorHandlers->displayErrorPage(ResponseStatus::PayloadTooLarge);
}
}
},
'methods' => ['POST'],
'types' => ['HTTP', 'XHR'],
],

'request.validateCsrf' => [
'action' => static function (Config $config, Request $request, Router $router, CsrfToken $csrfToken, ErrorHandlers $errorHandlers) {
if ($config->get('system.panel.enabled') && $router->requestHasPrefix($config->get('system.panel.root'))) {
return;
}

$tokenName = (string) $request->input()->get('csrf-token-name', 'site');
$token = (string) $request->input()->get('csrf-token');

if (!($csrfToken->validate($tokenName, $token))) {
$csrfToken->destroy($tokenName);
$errorHandlers->displayErrorPage(ResponseStatus::Forbidden);
}
},
'methods' => ['POST'],
'types' => ['HTTP', 'XHR'],
],

'language' => [
'action' => function (Config $config, Request $request, Router $router, Languages $languages) {
if (($requested = $languages->requested()) !== null) {
Expand Down
3 changes: 2 additions & 1 deletion formwork/src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ protected function loadServices(Container $container): void
->loader(ErrorHandlersServiceLoader::class)
->lazy(!$this->config()->get('system.errors.setHandlers', true));

$container->define(CsrfToken::class);
$container->define(CsrfToken::class)
->alias('csrfToken');

$container->define(Router::class)
->alias('router');
Expand Down
6 changes: 3 additions & 3 deletions formwork/src/Pages/Templates/Template.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Closure;
use Formwork\App;
use Formwork\Assets;
use Formwork\Pages\Page;
use Formwork\Pages\Site;
use Formwork\Utils\Constraint;
use Formwork\Utils\FileSystem;
Expand Down Expand Up @@ -98,8 +97,9 @@ public function render(array $vars = []): string
protected function defaultVars(): array
{
return [
'router' => $this->app->router(),
'site' => $this->site,
'router' => $this->app->router(),
'site' => $this->site,
'csrfToken' => $this->app->getService('csrfToken'),
];
}

Expand Down
2 changes: 1 addition & 1 deletion formwork/src/Panel/Controllers/AbstractController.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ protected function defaults(): array
'location' => $this->name,
'site' => $this->site(),
'panel' => $this->panel(),
'csrfToken' => $this->csrfToken->get(),
'csrfToken' => $this->csrfToken->get($this->panel()->getCsrfTokenName()),
'modals' => $this->modals(),
'colorScheme' => $this->getColorScheme(),
'navigation' => [
Expand Down
14 changes: 8 additions & 6 deletions formwork/src/Panel/Controllers/AuthenticationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ class AuthenticationController extends AbstractController
*/
public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $accessLimiter): Response
{
$csrfTokenName = $this->panel()->getCsrfTokenName();

if ($accessLimiter->hasReachedLimit()) {
$minutes = round($this->config->get('system.panel.loginResetTime') / 60);
$csrfToken->generate();
$csrfToken->generate($csrfTokenName);
return $this->error($this->translate('panel.login.attempt.tooMany', $minutes));
}

Expand All @@ -33,7 +35,7 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc
}

// Always generate a new CSRF token
$csrfToken->generate();
$csrfToken->generate($csrfTokenName);

return new Response($this->view('authentication.login', [
'title' => $this->translate('panel.login.login'),
Expand All @@ -47,7 +49,7 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc

// Ensure no required data is missing
if (!$data->hasMultiple(['username', 'password'])) {
$csrfToken->generate();
$csrfToken->generate($csrfTokenName);
$this->error($this->translate('panel.login.attempt.failed'));
}

Expand All @@ -61,7 +63,7 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc
$request->session()->set('FORMWORK_USERNAME', $data->get('username'));

// Regenerate CSRF token
$csrfToken->generate();
$csrfToken->generate($csrfTokenName);

$accessLog = new Log(FileSystem::joinPaths($this->config->get('system.panel.paths.logs'), 'access.json'));
$lastAccessRegistry = new Registry(FileSystem::joinPaths($this->config->get('system.panel.paths.logs'), 'lastAccess.json'));
Expand All @@ -79,7 +81,7 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc
return $this->redirect($this->generateRoute('panel.index'));
}

$csrfToken->generate();
$csrfToken->generate($csrfTokenName);
return $this->error($this->translate('panel.login.attempt.failed'), [
'username' => $data->get('username'),
'error' => true,
Expand All @@ -94,7 +96,7 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc
*/
public function logout(Request $request, CsrfToken $csrfToken): RedirectResponse
{
$csrfToken->destroy();
$csrfToken->destroy($this->panel()->getCsrfTokenName());
$request->session()->remove('FORMWORK_USERNAME');
$request->session()->destroy();

Expand Down
2 changes: 1 addition & 1 deletion formwork/src/Panel/Controllers/RegisterController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function register(Request $request, Schemes $schemes, CsrfToken $csrfToke
return $this->redirectToReferer();
}

$csrfToken->generate();
$csrfToken->generate($this->panel()->getCsrfTokenName());

$fields = $schemes->get('forms.register')->fields();

Expand Down
10 changes: 10 additions & 0 deletions formwork/src/Panel/Panel.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

final class Panel
{
protected const CSRF_TOKEN_NAME = 'panel';

/**
* Assets instance
*/
Expand Down Expand Up @@ -178,4 +180,12 @@ public function availableTranslations(): array

return $translations;
}

/**
* Get panel CSRF token name
*/
public function getCsrfTokenName(): string
{
return self::CSRF_TOKEN_NAME;
}
}
8 changes: 8 additions & 0 deletions formwork/src/Router/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ public function request(): string
return $this->requestUri;
}

/**
* Check if the request has the given prefix
*/
public function requestHasPrefix(string $prefix): bool
{
return $this->matchPrefix($prefix);
}

public function setRequest(string $request): void
{
$requestPath = Uri::path($request) ?? throw new UnexpectedValueException('Cannot get request path');
Expand Down
31 changes: 21 additions & 10 deletions formwork/src/Security/CsrfToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class CsrfToken
/**
* Session key to store the CSRF token
*/
protected const SESSION_KEY = 'CSRF_TOKEN';
protected const SESSION_KEY_PREFIX = '_formwork_csrf_tokens';

public function __construct(protected Request $request)
{
Expand All @@ -18,34 +18,45 @@ public function __construct(protected Request $request)
/**
* Generate a new CSRF token
*/
public function generate(): string
public function generate(string $name): string
{
$token = base64_encode(random_bytes(36));
$this->request->session()->set(self::SESSION_KEY, $token);
$this->request->session()->set(self::SESSION_KEY_PREFIX . '.' . $name, $token);
return $token;
}

/**
* Get current CSRF token
* Check if CSRF token exists
*/
public function get(): ?string
public function has(string $name): bool
{
return $this->request->session()->get(self::SESSION_KEY);
return $this->request->session()->has(self::SESSION_KEY_PREFIX . '.' . $name);
}

/**
* Get CSRF token by name
*/
public function get(string $name, bool $autoGenerate = false): ?string
{
if ($autoGenerate && !$this->has($name)) {
return $this->generate($name);
}
return $this->request->session()->get(self::SESSION_KEY_PREFIX . '.' . $name);
}

/**
* Check if given CSRF token is valid
*/
public function validate(string $token): bool
public function validate(string $name, string $token): bool
{
return ($storedToken = $this->get()) && hash_equals($token, $storedToken);
return ($storedToken = $this->get($name)) && hash_equals($token, $storedToken);
}

/**
* Remove CSRF token from session data
*/
public function destroy(): void
public function destroy(string $name): void
{
$this->request->session()->remove(self::SESSION_KEY);
$this->request->session()->remove(self::SESSION_KEY_PREFIX . '.' . $name);
}
}
12 changes: 7 additions & 5 deletions panel/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@
],

'filters' => [
'request.validateSize' => [
'panel.request.validateSize' => [
'action' => static function (Request $request, Translations $translations, Panel $panel) {
// Validate HTTP request Content-Length according to `post_max_size` directive
if ($request->contentLength() !== null) {
Expand All @@ -239,14 +239,16 @@
}
},
'methods' => ['POST'],
'types' => ['HTTP', 'XHR'],
],

'request.validateCsrf' => [
'panel.request.validateCsrf' => [
'action' => static function (Request $request, Translations $translations, Panel $panel, CsrfToken $csrfToken) {
$token = $request->input()->get('csrf-token');
$tokenName = $panel->getCsrfTokenName();
$token = (string) $request->input()->get('csrf-token');

if (!($token !== null && $csrfToken->validate($token))) {
$csrfToken->destroy();
if (!$csrfToken->validate($tokenName, $token)) {
$csrfToken->destroy($tokenName);
$request->session()->remove('FORMWORK_USERNAME');

$panel->notify(
Expand Down

0 comments on commit 3edb85e

Please sign in to comment.