diff --git a/system/HTTP/FormRequest.php b/system/HTTP/FormRequest.php index 624114060cda..28ad232c302d 100644 --- a/system/HTTP/FormRequest.php +++ b/system/HTTP/FormRequest.php @@ -153,14 +153,29 @@ protected function prepareForValidation(array $data): array * returns a 422 JSON response instead. * * @param array $errors + * @param array $preparedData */ - protected function failedValidation(array $errors): ResponseInterface + protected function failedValidation(array $errors, array $preparedData): ResponseInterface { if ($this->shouldReturnJsonResponse()) { return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]); } - return redirect()->back()->withInput(); + $redirect = redirect()->back()->withInput(); + + $key = in_array($this->request->getMethod(), [Method::GET, Method::HEAD], true) + ? 'get' + : 'post'; + + $oldInput = [ + 'get' => service('superglobals')->getGetArray(), + 'post' => service('superglobals')->getPostArray(), + ]; + $oldInput[$key] = $preparedData; + + service('session')->setFlashdata('_ci_old_input', $oldInput); + + return $redirect; } /** @@ -249,6 +264,8 @@ protected function validationData(): array */ final public function resolveRequest(): ?ResponseInterface { + $this->validatedData = []; + if (! $this->isAuthorized()) { return $this->failedAuthorization(); } @@ -259,7 +276,7 @@ final public function resolveRequest(): ?ResponseInterface ->setRules($this->rules(), $this->messages()); if (! $validation->run($data)) { - return $this->failedValidation($validation->getErrors()); + return $this->failedValidation($validation->getErrors(), $data); } $this->validatedData = $validation->getValidated(); diff --git a/tests/system/HTTP/FormRequestTest.php b/tests/system/HTTP/FormRequestTest.php index fe811fb3ee6a..d517b4cbb2bf 100644 --- a/tests/system/HTTP/FormRequestTest.php +++ b/tests/system/HTTP/FormRequestTest.php @@ -395,6 +395,142 @@ public function testResolveRequestRedirectsForWildcardAcceptHeader(): void $this->assertSame(303, $response->getStatusCode()); } + public function testPreparedValidationDataIsPassedToFailedValidationWithoutPreparingAgain(): void + { + service('superglobals')->setPost('title', ' Hello World '); + + $formRequest = new class ($this->makeRequest()) extends FormRequest { + public int $prepareCount = 0; + + /** + * @var array + */ + public array $preparedData = []; + + public function rules(): array + { + return [ + 'title' => 'required', + 'body' => 'required', + ]; + } + + protected function prepareForValidation(array $data): array + { + $this->prepareCount++; + $data['title'] = trim($data['title'] ?? ''); + + return $data; + } + + protected function failedValidation(array $errors, array $preparedData): ResponseInterface + { + $this->preparedData = $preparedData; + + return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]); + } + }; + + $formRequest->resolveRequest(); + + $this->assertSame(1, $formRequest->prepareCount); + $this->assertSame(['title' => 'Hello World'], $formRequest->preparedData); + } + + #[RunInSeparateProcess] + public function testDefaultFailedValidationFlashesPreparedValidationDataAsOldInput(): void + { + /** @var array $_SESSION */ + $_SESSION = []; + + service('superglobals')->setPost('title', ' Hello World '); + + $formRequest = new class ($this->makeRequest()) extends FormRequest { + public function rules(): array + { + return [ + 'title' => 'required', + 'body' => 'required', + ]; + } + + protected function prepareForValidation(array $data): array + { + $data['title'] = trim($data['title'] ?? ''); + + return $data; + } + }; + + $formRequest->resolveRequest(); + + $this->assertSame(['title' => 'Hello World'], $_SESSION['_ci_old_input']['post']); + } + + #[RunInSeparateProcess] + public function testDefaultFailedValidationFlashesPreparedGetDataAsOldInput(): void + { + /** @var array $_SESSION */ + $_SESSION = []; + + service('superglobals')->setServer('REQUEST_METHOD', 'GET'); + service('superglobals')->setGet('title', ' Hello World '); + + $formRequest = new class ($this->makeRequest()) extends FormRequest { + public function rules(): array + { + return [ + 'title' => 'required', + 'body' => 'required', + ]; + } + + protected function prepareForValidation(array $data): array + { + $data['title'] = trim($data['title'] ?? ''); + + return $data; + } + }; + + $formRequest->resolveRequest(); + + $this->assertSame(['title' => 'Hello World'], $_SESSION['_ci_old_input']['get']); + $this->assertSame([], $_SESSION['_ci_old_input']['post']); + } + + #[RunInSeparateProcess] + public function testDefaultFailedValidationPreservesGetDataWhenPostDataIsPrepared(): void + { + /** @var array $_SESSION */ + $_SESSION = []; + + service('superglobals')->setGet('category', '2'); + service('superglobals')->setPost('title', ' Hello World '); + + $formRequest = new class ($this->makeRequest()) extends FormRequest { + public function rules(): array + { + return [ + 'title' => 'required', + 'body' => 'required', + ]; + } + + protected function prepareForValidation(array $data): array + { + $data['title'] = trim($data['title'] ?? ''); + + return $data; + } + }; + + $formRequest->resolveRequest(); + + $this->assertSame(['category' => '2'], $_SESSION['_ci_old_input']['get']); + $this->assertSame(['title' => 'Hello World'], $_SESSION['_ci_old_input']['post']); + } + // ------------------------------------------------------------------------- // Authorization failure // ------------------------------------------------------------------------- @@ -488,7 +624,7 @@ public function rules(): array return ['title' => 'required']; } - protected function failedValidation(array $errors): ResponseInterface + protected function failedValidation(array $errors, array $preparedData): ResponseInterface { self::$called = true; diff --git a/user_guide_src/source/incoming/form_requests.rst b/user_guide_src/source/incoming/form_requests.rst index ebcd6ff28bd3..278703cd237e 100644 --- a/user_guide_src/source/incoming/form_requests.rst +++ b/user_guide_src/source/incoming/form_requests.rst @@ -171,10 +171,9 @@ normalized phone numbers, or trimmed strings. .. literalinclude:: form_requests/006.php :lines: 2- -.. note:: ``old()`` returns the original submitted input, not the normalized - values. Use ``getValidated()`` to access the processed data after a successful - request. If you need ``old()`` to reflect normalized values, see - :ref:`form-request-flash-normalized`. +.. note:: When validation fails and the default redirect response is used, + ``old()`` returns the prepared validation data. Use ``getValidated()`` to + access the processed data after a successful request. .. _form-request-validation-data: @@ -225,14 +224,19 @@ Flashing Normalized Input ========================= If your ``prepareForValidation()`` transforms visible form fields (for example, -trimming strings or canonicalizing values), ``old()`` will return the original -submitted input because the redirect flashes the raw superglobals. To make -``old()`` reflect the normalized values instead, override ``failedValidation()`` -and flash the normalized payload manually: +trimming strings or canonicalizing values), the default redirect response flashes +the prepared validation data as old input. + +If you override ``failedValidation()`` and still need to flash normalized input, +use the second ``$preparedData`` argument. It contains the same data that was +passed to validation: .. literalinclude:: form_requests/013.php :lines: 2- +The prepared data has not passed validation. After successful validation, use +``getValidated()`` or ``getValidatedInput()`` for trusted values. + ***************************************** How the Framework Resolves Form Requests ***************************************** diff --git a/user_guide_src/source/incoming/form_requests/008.php b/user_guide_src/source/incoming/form_requests/008.php index e5d99b9fe72a..98103e8fef7e 100644 --- a/user_guide_src/source/incoming/form_requests/008.php +++ b/user_guide_src/source/incoming/form_requests/008.php @@ -16,7 +16,7 @@ public function rules(): array } // Always respond with JSON, regardless of the request type. - protected function failedValidation(array $errors): ResponseInterface + protected function failedValidation(array $errors, array $preparedData): ResponseInterface { return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]); } diff --git a/user_guide_src/source/incoming/form_requests/013.php b/user_guide_src/source/incoming/form_requests/013.php index 92511ff3903b..041164fd43b7 100644 --- a/user_guide_src/source/incoming/form_requests/013.php +++ b/user_guide_src/source/incoming/form_requests/013.php @@ -22,8 +22,8 @@ protected function prepareForValidation(array $data): array return $data; } - // Override so that old() reflects the normalized values on redirect. - protected function failedValidation(array $errors): ResponseInterface + // Override while still flashing the prepared values on redirect. + protected function failedValidation(array $errors, array $preparedData): ResponseInterface { if ( $this->request->is('json') @@ -32,14 +32,13 @@ protected function failedValidation(array $errors): ResponseInterface return service('response')->setStatusCode(422)->setJSON(['errors' => $errors]); } - // withInput() flashes the original superglobals and the validation - // errors. We then overwrite old input with the normalized payload so - // that old() returns the same values that were validated. + // withInput() flashes validation errors. Then we replace old input with + // the same prepared values that were passed to validation. $redirect = redirect()->back()->withInput(); service('session')->setFlashdata('_ci_old_input', [ 'get' => [], - 'post' => $this->prepareForValidation($this->validationData()), + 'post' => $preparedData, ]); return $redirect;