|
| 1 | +<?php |
| 2 | +namespace Codeception\Lib\Connector; |
| 3 | + |
| 4 | +use Aws\Credentials\Credentials; |
| 5 | +use Aws\Signature\SignatureV4; |
| 6 | +use Codeception\Util\Uri; |
| 7 | +use GuzzleHttp\Client as GuzzleClient; |
| 8 | +use GuzzleHttp\Cookie\CookieJar; |
| 9 | +use GuzzleHttp\Cookie\SetCookie; |
| 10 | +use GuzzleHttp\Exception\RequestException; |
| 11 | +use GuzzleHttp\Handler\CurlHandler; |
| 12 | +use GuzzleHttp\Handler\StreamHandler; |
| 13 | +use GuzzleHttp\HandlerStack; |
| 14 | +use GuzzleHttp\Psr7\Request as Psr7Request; |
| 15 | +use GuzzleHttp\Psr7\Response as Psr7Response; |
| 16 | +use GuzzleHttp\Psr7\Uri as Psr7Uri; |
| 17 | +use Symfony\Component\BrowserKit\Client; |
| 18 | +use Symfony\Component\BrowserKit\Cookie; |
| 19 | +use Symfony\Component\BrowserKit\Request as BrowserKitRequest; |
| 20 | +use Symfony\Component\BrowserKit\Request; |
| 21 | +use Symfony\Component\BrowserKit\Response as BrowserKitResponse; |
| 22 | + |
| 23 | +class Guzzle extends Client |
| 24 | +{ |
| 25 | + protected $requestOptions = [ |
| 26 | + 'allow_redirects' => false, |
| 27 | + 'headers' => [], |
| 28 | + ]; |
| 29 | + protected $refreshMaxInterval = 0; |
| 30 | + |
| 31 | + protected $awsCredentials = null; |
| 32 | + protected $awsSignature = null; |
| 33 | + |
| 34 | + /** @var \GuzzleHttp\Client */ |
| 35 | + protected $client; |
| 36 | + |
| 37 | + /** |
| 38 | + * Sets the maximum allowable timeout interval for a meta tag refresh to |
| 39 | + * automatically redirect a request. |
| 40 | + * |
| 41 | + * A meta tag detected with an interval equal to or greater than $seconds |
| 42 | + * would not result in a redirect. A meta tag without a specified interval |
| 43 | + * or one with a value less than $seconds would result in the client |
| 44 | + * automatically redirecting to the specified URL |
| 45 | + * |
| 46 | + * @param int $seconds Number of seconds |
| 47 | + */ |
| 48 | + public function setRefreshMaxInterval($seconds) |
| 49 | + { |
| 50 | + $this->refreshMaxInterval = $seconds; |
| 51 | + } |
| 52 | + |
| 53 | + public function setClient(GuzzleClient &$client) |
| 54 | + { |
| 55 | + $this->client = $client; |
| 56 | + } |
| 57 | + |
| 58 | + /** |
| 59 | + * Sets the request header to the passed value. The header will be |
| 60 | + * sent along with the next request. |
| 61 | + * |
| 62 | + * Passing an empty value clears the header, which is the equivalent |
| 63 | + * of calling deleteHeader. |
| 64 | + * |
| 65 | + * @param string $name the name of the header |
| 66 | + * @param string $value the value of the header |
| 67 | + */ |
| 68 | + public function setHeader($name, $value) |
| 69 | + { |
| 70 | + if (strval($value) === '') { |
| 71 | + $this->deleteHeader($name); |
| 72 | + } else { |
| 73 | + $this->requestOptions['headers'][$name] = $value; |
| 74 | + } |
| 75 | + } |
| 76 | + |
| 77 | + /** |
| 78 | + * Deletes the header with the passed name from the list of headers |
| 79 | + * that will be sent with the request. |
| 80 | + * |
| 81 | + * @param string $name the name of the header to delete. |
| 82 | + */ |
| 83 | + public function deleteHeader($name) |
| 84 | + { |
| 85 | + unset($this->requestOptions['headers'][$name]); |
| 86 | + } |
| 87 | + |
| 88 | + /** |
| 89 | + * @param string $username |
| 90 | + * @param string $password |
| 91 | + * @param string $type Default: 'basic' |
| 92 | + */ |
| 93 | + public function setAuth($username, $password, $type = 'basic') |
| 94 | + { |
| 95 | + if (!$username) { |
| 96 | + unset($this->requestOptions['auth']); |
| 97 | + return; |
| 98 | + } |
| 99 | + $this->requestOptions['auth'] = [$username, $password, $type]; |
| 100 | + } |
| 101 | + |
| 102 | + /** |
| 103 | + * Taken from Mink\BrowserKitDriver |
| 104 | + * |
| 105 | + * @param Response $response |
| 106 | + * |
| 107 | + * @return \Symfony\Component\BrowserKit\Response |
| 108 | + */ |
| 109 | + protected function createResponse(Psr7Response $response) |
| 110 | + { |
| 111 | + $body = (string) $response->getBody(); |
| 112 | + $headers = $response->getHeaders(); |
| 113 | + |
| 114 | + $contentType = null; |
| 115 | + |
| 116 | + if (isset($headers['Content-Type'])) { |
| 117 | + $contentType = reset($headers['Content-Type']); |
| 118 | + } |
| 119 | + if (!$contentType) { |
| 120 | + $contentType = 'text/html'; |
| 121 | + } |
| 122 | + |
| 123 | + if (strpos($contentType, 'charset=') === false) { |
| 124 | + if (preg_match('/\<meta[^\>]+charset *= *["\']?([a-zA-Z\-0-9]+)/i', $body, $matches)) { |
| 125 | + $contentType .= ';charset=' . $matches[1]; |
| 126 | + } |
| 127 | + $headers['Content-Type'] = [$contentType]; |
| 128 | + } |
| 129 | + |
| 130 | + $status = $response->getStatusCode(); |
| 131 | + if ($status < 300 || $status >= 400) { |
| 132 | + $matches = []; |
| 133 | + |
| 134 | + $matchesMeta = preg_match( |
| 135 | + '/\<meta[^\>]+http-equiv="refresh" content="\s*(\d*)\s*;\s*url=(.*?)"/i', |
| 136 | + $body, |
| 137 | + $matches |
| 138 | + ); |
| 139 | + |
| 140 | + if (!$matchesMeta && isset($headers['Refresh'])) { |
| 141 | + // match by header |
| 142 | + preg_match( |
| 143 | + '/^\s*(\d*)\s*;\s*url=(.*)/i', |
| 144 | + (string) reset($headers['Refresh']), |
| 145 | + $matches |
| 146 | + ); |
| 147 | + } |
| 148 | + |
| 149 | + if ((!empty($matches)) && (empty($matches[1]) || $matches[1] < $this->refreshMaxInterval)) { |
| 150 | + $uri = new Psr7Uri($this->getAbsoluteUri($matches[2])); |
| 151 | + $currentUri = new Psr7Uri($this->getHistory()->current()->getUri()); |
| 152 | + |
| 153 | + if ($uri->withFragment('') != $currentUri->withFragment('')) { |
| 154 | + $status = 302; |
| 155 | + $headers['Location'] = $matchesMeta ? htmlspecialchars_decode($uri) : (string)$uri; |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + return new BrowserKitResponse($body, $status, $headers); |
| 161 | + } |
| 162 | + |
| 163 | + public function getAbsoluteUri($uri) |
| 164 | + { |
| 165 | + $baseUri = $this->client->getConfig('base_uri'); |
| 166 | + if (strpos($uri, '://') === false && strpos($uri, '//') !== 0) { |
| 167 | + if (strpos($uri, '/') === 0) { |
| 168 | + $baseUriPath = $baseUri->getPath(); |
| 169 | + if (!empty($baseUriPath) && strpos($uri, $baseUriPath) === 0) { |
| 170 | + $uri = substr($uri, strlen($baseUriPath)); |
| 171 | + } |
| 172 | + |
| 173 | + return Uri::appendPath((string)$baseUri, $uri); |
| 174 | + } |
| 175 | + // relative url |
| 176 | + if (!$this->getHistory()->isEmpty()) { |
| 177 | + return Uri::mergeUrls((string)$this->getHistory()->current()->getUri(), $uri); |
| 178 | + } |
| 179 | + } |
| 180 | + return Uri::mergeUrls($baseUri, $uri); |
| 181 | + } |
| 182 | + |
| 183 | + protected function doRequest($request) |
| 184 | + { |
| 185 | + /** @var $request BrowserKitRequest **/ |
| 186 | + $guzzleRequest = new Psr7Request( |
| 187 | + $request->getMethod(), |
| 188 | + $request->getUri(), |
| 189 | + $this->extractHeaders($request), |
| 190 | + $request->getContent() |
| 191 | + ); |
| 192 | + $options = $this->requestOptions; |
| 193 | + $options['cookies'] = $this->extractCookies($guzzleRequest->getUri()->getHost()); |
| 194 | + $multipartData = $this->extractMultipartFormData($request); |
| 195 | + if (!empty($multipartData)) { |
| 196 | + $options['multipart'] = $multipartData; |
| 197 | + } |
| 198 | + |
| 199 | + $formData = $this->extractFormData($request); |
| 200 | + if (empty($multipartData) and $formData) { |
| 201 | + $options['form_params'] = $formData; |
| 202 | + } |
| 203 | + |
| 204 | + try { |
| 205 | + if (null !== $this->awsCredentials) { |
| 206 | + $response = $this->client->send($this->awsSignature->signRequest($guzzleRequest, $this->awsCredentials), $options); |
| 207 | + } else { |
| 208 | + $response = $this->client->send($guzzleRequest, $options); |
| 209 | + } |
| 210 | + } catch (RequestException $e) { |
| 211 | + if (!$e->hasResponse()) { |
| 212 | + throw $e; |
| 213 | + } |
| 214 | + $response = $e->getResponse(); |
| 215 | + } |
| 216 | + return $this->createResponse($response); |
| 217 | + } |
| 218 | + |
| 219 | + protected function extractHeaders(BrowserKitRequest $request) |
| 220 | + { |
| 221 | + $headers = []; |
| 222 | + $server = $request->getServer(); |
| 223 | + |
| 224 | + $contentHeaders = ['Content-Length' => true, 'Content-Md5' => true, 'Content-Type' => true]; |
| 225 | + foreach ($server as $header => $val) { |
| 226 | + $header = html_entity_decode(implode('-', array_map('ucfirst', explode('-', strtolower(str_replace('_', '-', $header))))), ENT_NOQUOTES); |
| 227 | + if (strpos($header, 'Http-') === 0) { |
| 228 | + $headers[substr($header, 5)] = $val; |
| 229 | + } elseif (isset($contentHeaders[$header])) { |
| 230 | + $headers[$header] = $val; |
| 231 | + } |
| 232 | + } |
| 233 | + return $headers; |
| 234 | + } |
| 235 | + |
| 236 | + protected function extractFormData(BrowserKitRequest $request) |
| 237 | + { |
| 238 | + if (!in_array(strtoupper($request->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) { |
| 239 | + return null; |
| 240 | + } |
| 241 | + |
| 242 | + // guessing if it is a form data |
| 243 | + $headers = $request->getServer(); |
| 244 | + if (isset($headers['HTTP_CONTENT_TYPE'])) { |
| 245 | + // not a form |
| 246 | + if ($headers['HTTP_CONTENT_TYPE'] !== 'application/x-www-form-urlencoded') { |
| 247 | + return null; |
| 248 | + } |
| 249 | + } |
| 250 | + if ($request->getContent() !== null) { |
| 251 | + return null; |
| 252 | + } |
| 253 | + return $request->getParameters(); |
| 254 | + } |
| 255 | + |
| 256 | + protected function extractMultipartFormData(Request $request) |
| 257 | + { |
| 258 | + if (!in_array(strtoupper($request->getMethod()), ['POST', 'PUT', 'PATCH'])) { |
| 259 | + return []; |
| 260 | + } |
| 261 | + |
| 262 | + $parts = $this->mapFiles($request->getFiles()); |
| 263 | + if (empty($parts)) { |
| 264 | + return []; |
| 265 | + } |
| 266 | + |
| 267 | + foreach ($request->getParameters() as $k => $v) { |
| 268 | + $parts = $this->formatMultipart($parts, $k, $v); |
| 269 | + } |
| 270 | + return $parts; |
| 271 | + } |
| 272 | + |
| 273 | + protected function formatMultipart($parts, $key, $value) |
| 274 | + { |
| 275 | + if (is_array($value)) { |
| 276 | + foreach ($value as $subKey => $subValue) { |
| 277 | + $parts = array_merge($this->formatMultipart([], $key."[$subKey]", $subValue), $parts); |
| 278 | + } |
| 279 | + return $parts; |
| 280 | + } |
| 281 | + $parts[] = ['name' => $key, 'contents' => (string) $value]; |
| 282 | + return $parts; |
| 283 | + } |
| 284 | + |
| 285 | + protected function mapFiles($requestFiles, $arrayName = '') |
| 286 | + { |
| 287 | + $files = []; |
| 288 | + foreach ($requestFiles as $name => $info) { |
| 289 | + if (!empty($arrayName)) { |
| 290 | + $name = $arrayName . '[' . $name . ']'; |
| 291 | + } |
| 292 | + |
| 293 | + if (is_array($info)) { |
| 294 | + if (isset($info['tmp_name'])) { |
| 295 | + if ($info['tmp_name']) { |
| 296 | + $handle = fopen($info['tmp_name'], 'r'); |
| 297 | + $filename = isset($info['name']) ? $info['name'] : null; |
| 298 | + |
| 299 | + $files[] = [ |
| 300 | + 'name' => $name, |
| 301 | + 'contents' => $handle, |
| 302 | + 'filename' => $filename |
| 303 | + ]; |
| 304 | + } |
| 305 | + } else { |
| 306 | + $files = array_merge($files, $this->mapFiles($info, $name)); |
| 307 | + } |
| 308 | + } else { |
| 309 | + $files[] = [ |
| 310 | + 'name' => $name, |
| 311 | + 'contents' => fopen($info, 'r') |
| 312 | + ]; |
| 313 | + } |
| 314 | + } |
| 315 | + |
| 316 | + return $files; |
| 317 | + } |
| 318 | + |
| 319 | + protected function extractCookies($host) |
| 320 | + { |
| 321 | + $jar = []; |
| 322 | + $cookies = $this->getCookieJar()->all(); |
| 323 | + foreach ($cookies as $cookie) { |
| 324 | + /** @var $cookie Cookie **/ |
| 325 | + $setCookie = SetCookie::fromString((string)$cookie); |
| 326 | + if (!$setCookie->getDomain()) { |
| 327 | + $setCookie->setDomain($host); |
| 328 | + } |
| 329 | + $jar[] = $setCookie; |
| 330 | + } |
| 331 | + return new CookieJar(false, $jar); |
| 332 | + } |
| 333 | + |
| 334 | + public static function createHandler($handler) |
| 335 | + { |
| 336 | + if ($handler instanceof HandlerStack) { |
| 337 | + return $handler; |
| 338 | + } |
| 339 | + if ($handler === 'curl') { |
| 340 | + return HandlerStack::create(new CurlHandler()); |
| 341 | + } |
| 342 | + if ($handler === 'stream') { |
| 343 | + return HandlerStack::create(new StreamHandler()); |
| 344 | + } |
| 345 | + if (is_string($handler) && class_exists($handler)) { |
| 346 | + return HandlerStack::create(new $handler); |
| 347 | + } |
| 348 | + if (is_callable($handler)) { |
| 349 | + return HandlerStack::create($handler); |
| 350 | + } |
| 351 | + return HandlerStack::create(); |
| 352 | + } |
| 353 | + |
| 354 | + public function setAwsAuth($config) |
| 355 | + { |
| 356 | + $this->awsCredentials = new Credentials($config['key'], $config['secret']); |
| 357 | + $this->awsSignature = new SignatureV4($config['service'], $config['region']); |
| 358 | + } |
| 359 | +} |
0 commit comments