[CORE][Form] Add facilities for automattically adding a _next field to all forms, which can be customized by the in Form::create and defaults to the current URL. Usage of RedirectedException should mostly be replaced with Form::forceRedirect

This commit is contained in:
Hugo Sales 2022-03-04 15:12:35 +00:00
parent 7c9b01c516
commit 94449c9153
No known key found for this signature in database
GPG Key ID: 7D0C7EAFC9D835A0

View File

@ -33,13 +33,17 @@ declare(strict_types = 1);
namespace App\Core;
use App\Core\DB\DB;
use App\Core\Router\Router;
use App\Util\Common;
use App\Util\Exception\RedirectException;
use App\Util\Exception\ServerException;
use App\Util\Formatting;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Form as SymfForm;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface as SymfFormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
/**
@ -77,7 +81,6 @@ use Symfony\Component\HttpFoundation\Request;
abstract class Form
{
private static ?FormFactoryInterface $form_factory;
public static function setFactory($ff): void
{
self::$form_factory = $ff;
@ -85,18 +88,24 @@ abstract class Form
/**
* Create a form with the given associative array $form as fields
*
* If the current request has a GET parameter `next`, adds `_next` hidden. Default values gotten
* from the request, but can be overriden by `$extra_data['_next']`
*/
public static function create(
array $form,
?object $target = null,
array $extra_data = [],
string $type = 'Symfony\Component\Form\Extension\Core\Type\FormType',
string $type = '\Symfony\Component\Form\Extension\Core\Type\FormType',
array $form_options = [],
): SymfFormInterface {
$name = $form[array_key_last($form)][0];
$fb = self::$form_factory->createNamedBuilder($name, $type, data: null, options: array_merge($form_options, ['translation_domain' => false]));
$name = $form[array_key_last($form)][0];
$r = Common::getRequest();
$form[] = ['_next', HiddenType::class, ['data' => $r->get('next') ?? $r->get('_next') ?? $r->getRequestUri()]];
$fb = self::$form_factory->createNamedBuilder($name, $type, data: null, options: array_merge($form_options, ['translation_domain' => false]));
foreach ($form as [$key, $class, $options]) {
if ($class == SubmitType::class && \in_array($key, ['save', 'publish', 'post'])) {
if ($class == SubmitType::class && \in_array($key, ['save', 'publish', 'post', 'next'])) {
Log::critical($m = "It's generally a bad idea to use {$key} as a form name, because it can conflict with other forms in the same page");
throw new ServerException($m);
}
@ -195,10 +204,33 @@ abstract class Form
DB::merge($target);
DB::flush();
throw new RedirectException(url: $request->getPathInfo());
}
}
return $form;
}
/**
* Force a redirect to the `_next` form field, which is either a page to go after filling a
* form, or the page where the form was (given we use form actions). This prevents the browser
* from attempting to resubmit the form when the user merely ment to refresh the page
*/
public static function forceRedirect(SymfFormInterface $form, Request $request): RedirectResponse
{
$next = $form->get('_next')->getData();
try {
if ($pos = mb_strrpos($next, '#')) {
$fragment = mb_substr($next, $pos);
$next = mb_substr($next, 0, $pos);
}
Router::match($next);
$next = $next . ($fragment ?? '');
return new RedirectResponse(url: $next . ($fragment ?? ''));
} catch (ResourceNotFoundException $e) {
$user = Common::user();
$user_id = \is_null($user) ? $user->getId() : '(not logged in)';
Log::warning("Suspicious activity: User with ID {$user_id} submitted a form where the `_next` parameter is not a valid local URL ({$next})");
throw new ClientException(_m('Invalid form submission'), $e);
}
}
}