44
55namespace Http \Client \Common \Plugin ;
66
7+ use GuzzleHttp \Psr7 \Utils ;
78use Http \Client \Common \Exception \CircularRedirectionException ;
89use Http \Client \Common \Exception \MultipleRedirectionException ;
910use Http \Client \Common \Plugin ;
1011use Http \Client \Exception \HttpException ;
12+ use Http \Discovery \Psr17FactoryDiscovery ;
1113use Http \Promise \Promise ;
14+ use Nyholm \Psr7 \Factory \Psr17Factory ;
1215use Psr \Http \Message \RequestInterface ;
1316use Psr \Http \Message \ResponseInterface ;
17+ use Psr \Http \Message \StreamFactoryInterface ;
18+ use Psr \Http \Message \StreamInterface ;
1419use Psr \Http \Message \UriInterface ;
20+ use Symfony \Component \OptionsResolver \Options ;
1521use Symfony \Component \OptionsResolver \OptionsResolver ;
1622
1723/**
@@ -101,13 +107,19 @@ final class RedirectPlugin implements Plugin
101107 */
102108 private $ circularDetection = [];
103109
110+ /**
111+ * @var StreamFactoryInterface|null
112+ */
113+ private $ streamFactory ;
114+
104115 /**
105116 * @param array{'preserve_header'?: bool|string[], 'use_default_for_multiple'?: bool, 'strict'?: bool} $config
106117 *
107118 * Configuration options:
108119 * - preserve_header: True keeps all headers, false remove all of them, an array is interpreted as a list of header names to keep
109120 * - use_default_for_multiple: Whether the location header must be directly used for a multiple redirection status code (300)
110121 * - strict: When true, redirect codes 300, 301, 302 will not modify request method and body
122+ * - stream_factory: If set, must be a PSR-17 StreamFactoryInterface - if not set, we try to discover one
111123 */
112124 public function __construct (array $ config = [])
113125 {
@@ -116,17 +128,22 @@ public function __construct(array $config = [])
116128 'preserve_header ' => true ,
117129 'use_default_for_multiple ' => true ,
118130 'strict ' => false ,
131+ 'stream_factory ' => null ,
119132 ]);
120133 $ resolver ->setAllowedTypes ('preserve_header ' , ['bool ' , 'array ' ]);
121134 $ resolver ->setAllowedTypes ('use_default_for_multiple ' , 'bool ' );
122135 $ resolver ->setAllowedTypes ('strict ' , 'bool ' );
136+ $ resolver ->setAllowedTypes ('stream_factory ' , [StreamFactoryInterface::class, 'null ' ]);
123137 $ resolver ->setNormalizer ('preserve_header ' , function (OptionsResolver $ resolver , $ value ) {
124138 if (is_bool ($ value ) && false === $ value ) {
125139 return [];
126140 }
127141
128142 return $ value ;
129143 });
144+ $ resolver ->setDefault ('stream_factory ' , function (Options $ options ): ?StreamFactoryInterface {
145+ return $ this ->guessStreamFactory ();
146+ });
130147 $ options = $ resolver ->resolve ($ config );
131148
132149 $ this ->preserveHeader = $ options ['preserve_header ' ];
@@ -137,6 +154,8 @@ public function __construct(array $config = [])
137154 $ this ->redirectCodes [301 ]['switch ' ] = false ;
138155 $ this ->redirectCodes [302 ]['switch ' ] = false ;
139156 }
157+
158+ $ this ->streamFactory = $ options ['stream_factory ' ];
140159 }
141160
142161 /**
@@ -170,7 +189,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
170189
171190 $ this ->circularDetection [$ chainIdentifier ][] = (string ) $ request ->getUri ();
172191
173- if (in_array ((string ) $ redirectRequest ->getUri (), $ this ->circularDetection [$ chainIdentifier ])) {
192+ if (in_array ((string ) $ redirectRequest ->getUri (), $ this ->circularDetection [$ chainIdentifier ], true )) {
174193 throw new CircularRedirectionException ('Circular redirection detected ' , $ request , $ response );
175194 }
176195
@@ -186,19 +205,62 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
186205 });
187206 }
188207
208+ /**
209+ * The default only needs to be determined if no value is provided.
210+ */
211+ public function guessStreamFactory (): ?StreamFactoryInterface
212+ {
213+ if (class_exists (Psr17FactoryDiscovery::class)) {
214+ try {
215+ return Psr17FactoryDiscovery::findStreamFactory ();
216+ } catch (\Throwable $ t ) {
217+ // ignore and try other options
218+ }
219+ }
220+ if (class_exists (Psr17Factory::class)) {
221+ return new Psr17Factory ();
222+ }
223+ if (class_exists (Utils::class)) {
224+ return new class () implements StreamFactoryInterface {
225+ public function createStream (string $ content = '' ): StreamInterface
226+ {
227+ return Utils::streamFor ($ content );
228+ }
229+
230+ public function createStreamFromFile (string $ filename , string $ mode = 'r ' ): StreamInterface
231+ {
232+ throw new \RuntimeException ('Internal error: this method should not be needed ' );
233+ }
234+
235+ public function createStreamFromResource ($ resource ): StreamInterface
236+ {
237+ throw new \RuntimeException ('Internal error: this method should not be needed ' );
238+ }
239+ };
240+ }
241+
242+ return null ;
243+ }
244+
189245 private function buildRedirectRequest (RequestInterface $ originalRequest , UriInterface $ targetUri , int $ statusCode ): RequestInterface
190246 {
191247 $ originalRequest = $ originalRequest ->withUri ($ targetUri );
192248
193- if (false !== $ this ->redirectCodes [$ statusCode ]['switch ' ] && !in_array ($ originalRequest ->getMethod (), $ this ->redirectCodes [$ statusCode ]['switch ' ]['unless ' ])) {
249+ if (false !== $ this ->redirectCodes [$ statusCode ]['switch ' ] && !in_array ($ originalRequest ->getMethod (), $ this ->redirectCodes [$ statusCode ]['switch ' ]['unless ' ], true )) {
194250 $ originalRequest = $ originalRequest ->withMethod ($ this ->redirectCodes [$ statusCode ]['switch ' ]['to ' ]);
251+ if ('GET ' === $ this ->redirectCodes [$ statusCode ]['switch ' ]['to ' ] && $ this ->streamFactory ) {
252+ // if we found a stream factory, remove the request body. otherwise leave the body there.
253+ $ originalRequest = $ originalRequest ->withoutHeader ('content-type ' );
254+ $ originalRequest = $ originalRequest ->withoutHeader ('content-length ' );
255+ $ originalRequest = $ originalRequest ->withBody ($ this ->streamFactory ->createStream ());
256+ }
195257 }
196258
197259 if (is_array ($ this ->preserveHeader )) {
198260 $ headers = array_keys ($ originalRequest ->getHeaders ());
199261
200262 foreach ($ headers as $ name ) {
201- if (!in_array ($ name , $ this ->preserveHeader )) {
263+ if (!in_array ($ name , $ this ->preserveHeader , true )) {
202264 $ originalRequest = $ originalRequest ->withoutHeader ($ name );
203265 }
204266 }
0 commit comments