4
4
5
5
namespace Http \Client \Common \Plugin ;
6
6
7
+ use GuzzleHttp \Psr7 \Utils ;
7
8
use Http \Client \Common \Exception \CircularRedirectionException ;
8
9
use Http \Client \Common \Exception \MultipleRedirectionException ;
9
10
use Http \Client \Common \Plugin ;
10
11
use Http \Client \Exception \HttpException ;
12
+ use Http \Discovery \Psr17FactoryDiscovery ;
11
13
use Http \Promise \Promise ;
14
+ use Nyholm \Psr7 \Factory \Psr17Factory ;
12
15
use Psr \Http \Message \RequestInterface ;
13
16
use Psr \Http \Message \ResponseInterface ;
17
+ use Psr \Http \Message \StreamFactoryInterface ;
18
+ use Psr \Http \Message \StreamInterface ;
14
19
use Psr \Http \Message \UriInterface ;
20
+ use Symfony \Component \OptionsResolver \Options ;
15
21
use Symfony \Component \OptionsResolver \OptionsResolver ;
16
22
17
23
/**
@@ -101,13 +107,19 @@ final class RedirectPlugin implements Plugin
101
107
*/
102
108
private $ circularDetection = [];
103
109
110
+ /**
111
+ * @var StreamFactoryInterface|null
112
+ */
113
+ private $ streamFactory ;
114
+
104
115
/**
105
116
* @param array{'preserve_header'?: bool|string[], 'use_default_for_multiple'?: bool, 'strict'?: bool} $config
106
117
*
107
118
* Configuration options:
108
119
* - preserve_header: True keeps all headers, false remove all of them, an array is interpreted as a list of header names to keep
109
120
* - use_default_for_multiple: Whether the location header must be directly used for a multiple redirection status code (300)
110
121
* - 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
111
123
*/
112
124
public function __construct (array $ config = [])
113
125
{
@@ -116,17 +128,22 @@ public function __construct(array $config = [])
116
128
'preserve_header ' => true ,
117
129
'use_default_for_multiple ' => true ,
118
130
'strict ' => false ,
131
+ 'stream_factory ' => null ,
119
132
]);
120
133
$ resolver ->setAllowedTypes ('preserve_header ' , ['bool ' , 'array ' ]);
121
134
$ resolver ->setAllowedTypes ('use_default_for_multiple ' , 'bool ' );
122
135
$ resolver ->setAllowedTypes ('strict ' , 'bool ' );
136
+ $ resolver ->setAllowedTypes ('stream_factory ' , [StreamFactoryInterface::class, 'null ' ]);
123
137
$ resolver ->setNormalizer ('preserve_header ' , function (OptionsResolver $ resolver , $ value ) {
124
138
if (is_bool ($ value ) && false === $ value ) {
125
139
return [];
126
140
}
127
141
128
142
return $ value ;
129
143
});
144
+ $ resolver ->setDefault ('stream_factory ' , function (Options $ options ): ?StreamFactoryInterface {
145
+ return $ this ->guessStreamFactory ();
146
+ });
130
147
$ options = $ resolver ->resolve ($ config );
131
148
132
149
$ this ->preserveHeader = $ options ['preserve_header ' ];
@@ -137,6 +154,8 @@ public function __construct(array $config = [])
137
154
$ this ->redirectCodes [301 ]['switch ' ] = false ;
138
155
$ this ->redirectCodes [302 ]['switch ' ] = false ;
139
156
}
157
+
158
+ $ this ->streamFactory = $ options ['stream_factory ' ];
140
159
}
141
160
142
161
/**
@@ -170,7 +189,7 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
170
189
171
190
$ this ->circularDetection [$ chainIdentifier ][] = (string ) $ request ->getUri ();
172
191
173
- if (in_array ((string ) $ redirectRequest ->getUri (), $ this ->circularDetection [$ chainIdentifier ])) {
192
+ if (in_array ((string ) $ redirectRequest ->getUri (), $ this ->circularDetection [$ chainIdentifier ], true )) {
174
193
throw new CircularRedirectionException ('Circular redirection detected ' , $ request , $ response );
175
194
}
176
195
@@ -186,19 +205,62 @@ public function handleRequest(RequestInterface $request, callable $next, callabl
186
205
});
187
206
}
188
207
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
+
189
245
private function buildRedirectRequest (RequestInterface $ originalRequest , UriInterface $ targetUri , int $ statusCode ): RequestInterface
190
246
{
191
247
$ originalRequest = $ originalRequest ->withUri ($ targetUri );
192
248
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 )) {
194
250
$ 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
+ }
195
257
}
196
258
197
259
if (is_array ($ this ->preserveHeader )) {
198
260
$ headers = array_keys ($ originalRequest ->getHeaders ());
199
261
200
262
foreach ($ headers as $ name ) {
201
- if (!in_array ($ name , $ this ->preserveHeader )) {
263
+ if (!in_array ($ name , $ this ->preserveHeader , true )) {
202
264
$ originalRequest = $ originalRequest ->withoutHeader ($ name );
203
265
}
204
266
}
0 commit comments