diff --git a/src/Core/Form.php b/src/Core/Form.php index 239d6e0159..32e6c4f11e 100644 --- a/src/Core/Form.php +++ b/src/Core/Form.php @@ -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); + } + } }