33namespace Tonysm \TurboLaravel \Http \Middleware ;
44
55use Closure ;
6+ use Illuminate \Contracts \Http \Kernel ;
67use Illuminate \Http \RedirectResponse ;
78use Illuminate \Http \Request ;
9+ use Illuminate \Http \Response ;
10+ use Illuminate \Support \Facades \App ;
11+ use Illuminate \Support \Facades \Facade ;
812use Illuminate \Support \Facades \Route ;
913use Illuminate \Support \Str ;
1014use Illuminate \Validation \ValidationException ;
15+ use Symfony \Component \HttpFoundation \Cookie ;
1116use Tonysm \TurboLaravel \Facades \Turbo as TurboFacade ;
1217use Tonysm \TurboLaravel \Turbo ;
1318
@@ -16,6 +21,13 @@ class TurboMiddleware
1621 /** @var \Tonysm\TurboLaravel\Http\Middleware\RouteRedirectGuesser */
1722 private $ redirectGuesser ;
1823
24+ /**
25+ * Encrypted cookies to be added to the internal requests following redirects.
26+ *
27+ * @var array
28+ */
29+ private array $ encryptedCookies ;
30+
1931 public function __construct (RouteRedirectGuesser $ redirectGuesser )
2032 {
2133 $ this ->redirectGuesser = $ redirectGuesser ;
@@ -28,6 +40,8 @@ public function __construct(RouteRedirectGuesser $redirectGuesser)
2840 */
2941 public function handle ($ request , Closure $ next )
3042 {
43+ $ this ->encryptedCookies = $ request ->cookies ->all ();
44+
3145 if ($ this ->turboNativeVisit ($ request )) {
3246 TurboFacade::setVisitingFromTurboNative ();
3347 }
@@ -59,21 +73,79 @@ private function turboResponse($response, Request $request)
5973 return $ response ;
6074 }
6175
62- // Turbo expects a 303 redirect. We are also changing the default behavior of Laravel's failed
63- // validation redirection to send the user to a page where the form of the current resource
64- // is rendered (instead of just "back"), since Frames could have been used in many pages.
76+ // We get the response's encrypted cookies and merge them with the
77+ // encrypted cookies of the first request to make sure that are
78+ // sub-sequent request will use the most up-to-date values.
79+
80+ $ responseCookies = collect ($ response ->headers ->getCookies ())
81+ ->mapWithKeys (fn (Cookie $ cookie ) => [$ cookie ->getName () => $ cookie ->getValue ()])
82+ ->all ();
83+
84+ $ this ->encryptedCookies = array_replace_recursive ($ this ->encryptedCookies , $ responseCookies );
85+
86+ // When throwing a ValidationException and the app uses named routes convention, we can guess
87+ // the form route for the current endpoint, make an internal request there, and return the
88+ // response body with the form over a 422 status code, which is better for Turbo Native.
89+
90+ if ($ response ->exception instanceof ValidationException && ($ formRedirectUrl = $ this ->getRedirectUrl ($ request , $ response ))) {
91+ $ response ->setTargetUrl ($ formRedirectUrl );
92+
93+ return tap ($ this ->handleRedirectInternally ($ request , $ response ), function () use ($ request ) {
94+ App::instance ('request ' , $ request );
95+ Facade::clearResolvedInstance ('request ' );
96+ });
97+ }
98+
99+ return $ response ->setStatusCode (303 );
100+ }
101+
102+ private function getRedirectUrl ($ request , $ response )
103+ {
104+ if ($ response ->exception ->redirectTo ) {
105+ return $ response ->exception ->redirectTo ;
106+ }
107+
108+ return $ this ->guessFormRedirectUrl ($ request );
109+ }
65110
66- $ response ->setStatusCode (303 );
111+ private function kernel (): Kernel
112+ {
113+ return App::make (Kernel::class);
114+ }
67115
68- if ($ response ->exception instanceof ValidationException && ! $ response ->exception ->redirectTo ) {
69- $ response ->setTargetUrl (
70- $ this ->guessRedirectingRoute ($ request ) ?: $ response ->getTargetUrl ()
116+ /**
117+ * @param Request $request
118+ * @param Response $response
119+ *
120+ * @return Response
121+ */
122+ private function handleRedirectInternally ($ request , $ response )
123+ {
124+ $ kernel = $ this ->kernel ();
125+
126+ do {
127+ $ response = $ kernel ->handle (
128+ $ request = $ this ->createRequestFrom ($ response ->headers ->get ('Location ' ), $ request )
71129 );
130+ } while ($ response ->isRedirect ());
131+
132+ if ($ response ->isOk ()) {
133+ $ response ->setStatusCode (422 );
72134 }
73135
74136 return $ response ;
75137 }
76138
139+ private function createRequestFrom (string $ url , Request $ baseRequest )
140+ {
141+ $ request = Request::create ($ url , 'GET ' );
142+
143+ $ request ->headers ->replace ($ baseRequest ->headers ->all ());
144+ $ request ->cookies ->replace ($ this ->encryptedCookies );
145+
146+ return $ request ;
147+ }
148+
77149 /**
78150 * @param \Illuminate\Http\Request $request
79151 * @return bool
@@ -86,7 +158,7 @@ private function turboVisit($request)
86158 /**
87159 * @param \Illuminate\Http\Request $request
88160 */
89- private function guessRedirectingRoute ($ request )
161+ private function guessFormRedirectUrl ($ request )
90162 {
91163 $ route = $ request ->route ();
92164 $ name = optional ($ route )->getName ();
@@ -99,7 +171,7 @@ private function guessRedirectingRoute($request)
99171
100172 // If the guessed route doesn't exist, send it back to wherever Laravel defaults to.
101173
102- if (! Route::has ($ formRouteName )) {
174+ if (! $ formRouteName || ! Route::has ($ formRouteName )) {
103175 return null ;
104176 }
105177
0 commit comments