@@ -223,4 +223,86 @@ public function testResponseDurationIsTrackedOnError(): void
223223 $ this ->assertGreaterThanOrEqual (0 , $ e ->getDuration ());
224224 }
225225 }
226+
227+ public function test303RedirectPreservesHeadMethod (): void
228+ {
229+ // Directly test that a 303 redirect preserves HEAD method
230+ // We'll use reflection to call the protected request() method with HEAD
231+
232+ $ redirectResponse = new PsrResponse (303 , ['Location ' => 'https://example.com/new-feed.xml ' ]);
233+ $ finalResponse = new PsrResponse (200 , [], 'content ' );
234+
235+ $ requestCount = 0 ;
236+ $ this ->psrClient
237+ ->expects ($ this ->exactly (2 ))
238+ ->method ('sendRequest ' )
239+ ->willReturnCallback (function (RequestInterface $ request ) use ($ redirectResponse , $ finalResponse , &$ requestCount ) {
240+ $ requestCount ++;
241+
242+ if ($ requestCount === 1 ) {
243+ // First request: HEAD
244+ $ this ->assertEquals ('HEAD ' , $ request ->getMethod ());
245+ return $ redirectResponse ;
246+ }
247+
248+ // Second request: should still be HEAD (not changed to GET for 303)
249+ $ this ->assertEquals ('HEAD ' , $ request ->getMethod ());
250+ $ this ->assertEquals ('https://example.com/new-feed.xml ' , (string ) $ request ->getUri ());
251+ return $ finalResponse ;
252+ });
253+
254+ // Use reflection to call protected request() method with HEAD
255+ $ reflection = new \ReflectionClass ($ this ->client );
256+ $ method = $ reflection ->getMethod ('request ' );
257+ $ method ->setAccessible (true );
258+
259+ $ response = $ method ->invoke ($ this ->client , 'HEAD ' , 'https://example.com/old-feed.xml ' , null , 0 );
260+
261+ $ this ->assertEquals (200 , $ response ->getStatusCode ());
262+ }
263+
264+ /**
265+ * @dataProvider maliciousSchemeProvider
266+ */
267+ public function testRejectsRedirectsWithMaliciousSchemes (string $ maliciousUrl ): void
268+ {
269+ $ redirectResponse = new PsrResponse (301 , ['Location ' => $ maliciousUrl ]);
270+
271+ $ this ->psrClient
272+ ->expects ($ this ->once ())
273+ ->method ('sendRequest ' )
274+ ->willReturn ($ redirectResponse );
275+
276+ $ this ->expectException (ServerErrorException::class);
277+
278+ $ this ->client ->getResponse ('https://example.com/feed.xml ' );
279+ }
280+
281+ public static function maliciousSchemeProvider (): array
282+ {
283+ return [
284+ 'file scheme ' => ['file:///etc/passwd ' ],
285+ 'ftp scheme ' => ['ftp://malicious.com/data ' ],
286+ 'javascript scheme ' => ['javascript:alert(1) ' ],
287+ 'data scheme ' => ['data:text/html,<script>alert(1)</script> ' ],
288+ 'mailto scheme ' => [
'mailto:[email protected] ' ],
289+ 'tel scheme ' => ['tel:+1234567890 ' ],
290+ ];
291+ }
292+
293+ public function testAllowsHttpAndHttpsRedirects (): void
294+ {
295+ $ httpRedirect = new PsrResponse (301 , ['Location ' => 'http://example.com/feed.xml ' ]);
296+ $ httpsRedirect = new PsrResponse (301 , ['Location ' => 'https://secure.example.com/feed.xml ' ]);
297+ $ finalResponse = new PsrResponse (200 , [], 'content ' );
298+
299+ $ this ->psrClient
300+ ->expects ($ this ->exactly (3 ))
301+ ->method ('sendRequest ' )
302+ ->willReturnOnConsecutiveCalls ($ httpRedirect , $ httpsRedirect , $ finalResponse );
303+
304+ $ response = $ this ->client ->getResponse ('https://example.com/feed.xml ' );
305+
306+ $ this ->assertEquals (200 , $ response ->getStatusCode ());
307+ }
226308}
0 commit comments