[ Index ]

PHP Cross Reference of phpBB-3.2.11-deutsch

title

Body

[close]

/vendor/guzzlehttp/ringphp/src/Client/ -> CurlFactory.php (source)

   1  <?php
   2  namespace GuzzleHttp\Ring\Client;
   3  
   4  use GuzzleHttp\Ring\Core;
   5  use GuzzleHttp\Ring\Exception\ConnectException;
   6  use GuzzleHttp\Ring\Exception\RingException;
   7  use GuzzleHttp\Stream\LazyOpenStream;
   8  use GuzzleHttp\Stream\StreamInterface;
   9  
  10  /**
  11   * Creates curl resources from a request
  12   */
  13  class CurlFactory
  14  {
  15      /**
  16       * Creates a cURL handle, header resource, and body resource based on a
  17       * transaction.
  18       *
  19       * @param array         $request Request hash
  20       * @param null|resource $handle  Optionally provide a curl handle to modify
  21       *
  22       * @return array Returns an array of the curl handle, headers array, and
  23       *               response body handle.
  24       * @throws \RuntimeException when an option cannot be applied
  25       */
  26      public function __invoke(array $request, $handle = null)
  27      {
  28          $headers = [];
  29          $options = $this->getDefaultOptions($request, $headers);
  30          $this->applyMethod($request, $options);
  31  
  32          if (isset($request['client'])) {
  33              $this->applyHandlerOptions($request, $options);
  34          }
  35  
  36          $this->applyHeaders($request, $options);
  37          unset($options['_headers']);
  38  
  39          // Add handler options from the request's configuration options
  40          if (isset($request['client']['curl'])) {
  41              $options = $this->applyCustomCurlOptions(
  42                  $request['client']['curl'],
  43                  $options
  44              );
  45          }
  46  
  47          if (!$handle) {
  48              $handle = curl_init();
  49          }
  50  
  51          $body = $this->getOutputBody($request, $options);
  52          curl_setopt_array($handle, $options);
  53  
  54          return [$handle, &$headers, $body];
  55      }
  56  
  57      /**
  58       * Creates a response hash from a cURL result.
  59       *
  60       * @param callable $handler  Handler that was used.
  61       * @param array    $request  Request that sent.
  62       * @param array    $response Response hash to update.
  63       * @param array    $headers  Headers received during transfer.
  64       * @param resource $body     Body fopen response.
  65       *
  66       * @return array
  67       */
  68      public static function createResponse(
  69          callable $handler,
  70          array $request,
  71          array $response,
  72          array $headers,
  73          $body
  74      ) {
  75          if (isset($response['transfer_stats']['url'])) {
  76              $response['effective_url'] = $response['transfer_stats']['url'];
  77          }
  78  
  79          if (!empty($headers)) {
  80              $startLine = explode(' ', array_shift($headers), 3);
  81              $headerList = Core::headersFromLines($headers);
  82              $response['headers'] = $headerList;
  83              $response['version'] = isset($startLine[0]) ? substr($startLine[0], 5) : null;
  84              $response['status'] = isset($startLine[1]) ? (int) $startLine[1] : null;
  85              $response['reason'] = isset($startLine[2]) ? $startLine[2] : null;
  86              $response['body'] = $body;
  87              Core::rewindBody($response);
  88          }
  89  
  90          return !empty($response['curl']['errno']) || !isset($response['status'])
  91              ? self::createErrorResponse($handler, $request, $response)
  92              : $response;
  93      }
  94  
  95      private static function createErrorResponse(
  96          callable $handler,
  97          array $request,
  98          array $response
  99      ) {
 100          static $connectionErrors = [
 101              CURLE_OPERATION_TIMEOUTED  => true,
 102              CURLE_COULDNT_RESOLVE_HOST => true,
 103              CURLE_COULDNT_CONNECT      => true,
 104              CURLE_SSL_CONNECT_ERROR    => true,
 105              CURLE_GOT_NOTHING          => true,
 106          ];
 107  
 108          // Retry when nothing is present or when curl failed to rewind.
 109          if (!isset($response['err_message'])
 110              && (empty($response['curl']['errno'])
 111                  || $response['curl']['errno'] == 65)
 112          ) {
 113              return self::retryFailedRewind($handler, $request, $response);
 114          }
 115  
 116          $message = isset($response['err_message'])
 117              ? $response['err_message']
 118              : sprintf('cURL error %s: %s',
 119                  $response['curl']['errno'],
 120                  isset($response['curl']['error'])
 121                      ? $response['curl']['error']
 122                      : 'See http://curl.haxx.se/libcurl/c/libcurl-errors.html');
 123  
 124          $error = isset($response['curl']['errno'])
 125              && isset($connectionErrors[$response['curl']['errno']])
 126              ? new ConnectException($message)
 127              : new RingException($message);
 128  
 129          return $response + [
 130              'status'  => null,
 131              'reason'  => null,
 132              'body'    => null,
 133              'headers' => [],
 134              'error'   => $error,
 135          ];
 136      }
 137  
 138      private function getOutputBody(array $request, array &$options)
 139      {
 140          // Determine where the body of the response (if any) will be streamed.
 141          if (isset($options[CURLOPT_WRITEFUNCTION])) {
 142              return $request['client']['save_to'];
 143          }
 144  
 145          if (isset($options[CURLOPT_FILE])) {
 146              return $options[CURLOPT_FILE];
 147          }
 148  
 149          if ($request['http_method'] != 'HEAD') {
 150              // Create a default body if one was not provided
 151              return $options[CURLOPT_FILE] = fopen('php://temp', 'w+');
 152          }
 153  
 154          return null;
 155      }
 156  
 157      private function getDefaultOptions(array $request, array &$headers)
 158      {
 159          $url = Core::url($request);
 160          $startingResponse = false;
 161  
 162          $options = [
 163              '_headers'             => $request['headers'],
 164              CURLOPT_CUSTOMREQUEST  => $request['http_method'],
 165              CURLOPT_URL            => $url,
 166              CURLOPT_RETURNTRANSFER => false,
 167              CURLOPT_HEADER         => false,
 168              CURLOPT_CONNECTTIMEOUT => 150,
 169              CURLOPT_HEADERFUNCTION => function ($ch, $h) use (&$headers, &$startingResponse) {
 170                  $value = trim($h);
 171                  if ($value === '') {
 172                      $startingResponse = true;
 173                  } elseif ($startingResponse) {
 174                      $startingResponse = false;
 175                      $headers = [$value];
 176                  } else {
 177                      $headers[] = $value;
 178                  }
 179                  return strlen($h);
 180              },
 181          ];
 182  
 183          if (isset($request['version'])) {
 184              if ($request['version'] == 2.0) {
 185                  $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0;
 186              } else if ($request['version'] == 1.1) {
 187                  $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
 188              } else {
 189                  $options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_0;
 190              }
 191          }
 192  
 193          if (defined('CURLOPT_PROTOCOLS')) {
 194              $options[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
 195          }
 196  
 197          return $options;
 198      }
 199  
 200      private function applyMethod(array $request, array &$options)
 201      {
 202          if (isset($request['body'])) {
 203              $this->applyBody($request, $options);
 204              return;
 205          }
 206  
 207          switch ($request['http_method']) {
 208              case 'PUT':
 209              case 'POST':
 210                  // See http://tools.ietf.org/html/rfc7230#section-3.3.2
 211                  if (!Core::hasHeader($request, 'Content-Length')) {
 212                      $options[CURLOPT_HTTPHEADER][] = 'Content-Length: 0';
 213                  }
 214                  break;
 215              case 'HEAD':
 216                  $options[CURLOPT_NOBODY] = true;
 217                  unset(
 218                      $options[CURLOPT_WRITEFUNCTION],
 219                      $options[CURLOPT_READFUNCTION],
 220                      $options[CURLOPT_FILE],
 221                      $options[CURLOPT_INFILE]
 222                  );
 223          }
 224      }
 225  
 226      private function applyBody(array $request, array &$options)
 227      {
 228          $contentLength = Core::firstHeader($request, 'Content-Length');
 229          $size = $contentLength !== null ? (int) $contentLength : null;
 230  
 231          // Send the body as a string if the size is less than 1MB OR if the
 232          // [client][curl][body_as_string] request value is set.
 233          if (($size !== null && $size < 1000000) ||
 234              isset($request['client']['curl']['body_as_string']) ||
 235              is_string($request['body'])
 236          ) {
 237              $options[CURLOPT_POSTFIELDS] = Core::body($request);
 238              // Don't duplicate the Content-Length header
 239              $this->removeHeader('Content-Length', $options);
 240              $this->removeHeader('Transfer-Encoding', $options);
 241          } else {
 242              $options[CURLOPT_UPLOAD] = true;
 243              if ($size !== null) {
 244                  // Let cURL handle setting the Content-Length header
 245                  $options[CURLOPT_INFILESIZE] = $size;
 246                  $this->removeHeader('Content-Length', $options);
 247              }
 248              $this->addStreamingBody($request, $options);
 249          }
 250  
 251          // If the Expect header is not present, prevent curl from adding it
 252          if (!Core::hasHeader($request, 'Expect')) {
 253              $options[CURLOPT_HTTPHEADER][] = 'Expect:';
 254          }
 255  
 256          // cURL sometimes adds a content-type by default. Prevent this.
 257          if (!Core::hasHeader($request, 'Content-Type')) {
 258              $options[CURLOPT_HTTPHEADER][] = 'Content-Type:';
 259          }
 260      }
 261  
 262      private function addStreamingBody(array $request, array &$options)
 263      {
 264          $body = $request['body'];
 265  
 266          if ($body instanceof StreamInterface) {
 267              $options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
 268                  return (string) $body->read($length);
 269              };
 270              if (!isset($options[CURLOPT_INFILESIZE])) {
 271                  if ($size = $body->getSize()) {
 272                      $options[CURLOPT_INFILESIZE] = $size;
 273                  }
 274              }
 275          } elseif (is_resource($body)) {
 276              $options[CURLOPT_INFILE] = $body;
 277          } elseif ($body instanceof \Iterator) {
 278              $buf = '';
 279              $options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body, &$buf) {
 280                  if ($body->valid()) {
 281                      $buf .= $body->current();
 282                      $body->next();
 283                  }
 284                  $result = (string) substr($buf, 0, $length);
 285                  $buf = substr($buf, $length);
 286                  return $result;
 287              };
 288          } else {
 289              throw new \InvalidArgumentException('Invalid request body provided');
 290          }
 291      }
 292  
 293      private function applyHeaders(array $request, array &$options)
 294      {
 295          foreach ($options['_headers'] as $name => $values) {
 296              foreach ($values as $value) {
 297                  $options[CURLOPT_HTTPHEADER][] = "$name: $value";
 298              }
 299          }
 300  
 301          // Remove the Accept header if one was not set
 302          if (!Core::hasHeader($request, 'Accept')) {
 303              $options[CURLOPT_HTTPHEADER][] = 'Accept:';
 304          }
 305      }
 306  
 307      /**
 308       * Takes an array of curl options specified in the 'curl' option of a
 309       * request's configuration array and maps them to CURLOPT_* options.
 310       *
 311       * This method is only called when a  request has a 'curl' config setting.
 312       *
 313       * @param array $config  Configuration array of custom curl option
 314       * @param array $options Array of existing curl options
 315       *
 316       * @return array Returns a new array of curl options
 317       */
 318      private function applyCustomCurlOptions(array $config, array $options)
 319      {
 320          $curlOptions = [];
 321          foreach ($config as $key => $value) {
 322              if (is_int($key)) {
 323                  $curlOptions[$key] = $value;
 324              }
 325          }
 326  
 327          return $curlOptions + $options;
 328      }
 329  
 330      /**
 331       * Remove a header from the options array.
 332       *
 333       * @param string $name    Case-insensitive header to remove
 334       * @param array  $options Array of options to modify
 335       */
 336      private function removeHeader($name, array &$options)
 337      {
 338          foreach (array_keys($options['_headers']) as $key) {
 339              if (!strcasecmp($key, $name)) {
 340                  unset($options['_headers'][$key]);
 341                  return;
 342              }
 343          }
 344      }
 345  
 346      /**
 347       * Applies an array of request client options to a the options array.
 348       *
 349       * This method uses a large switch rather than double-dispatch to save on
 350       * high overhead of calling functions in PHP.
 351       */
 352      private function applyHandlerOptions(array $request, array &$options)
 353      {
 354          foreach ($request['client'] as $key => $value) {
 355              switch ($key) {
 356              // Violating PSR-4 to provide more room.
 357              case 'verify':
 358  
 359                  if ($value === false) {
 360                      unset($options[CURLOPT_CAINFO]);
 361                      $options[CURLOPT_SSL_VERIFYHOST] = 0;
 362                      $options[CURLOPT_SSL_VERIFYPEER] = false;
 363                      continue 2;
 364                  }
 365  
 366                  $options[CURLOPT_SSL_VERIFYHOST] = 2;
 367                  $options[CURLOPT_SSL_VERIFYPEER] = true;
 368  
 369                  if (is_string($value)) {
 370                      $options[CURLOPT_CAINFO] = $value;
 371                      if (!file_exists($value)) {
 372                          throw new \InvalidArgumentException(
 373                              "SSL CA bundle not found: $value"
 374                          );
 375                      }
 376                  }
 377                  break;
 378  
 379              case 'decode_content':
 380  
 381                  if ($value === false) {
 382                      continue 2;
 383                  }
 384  
 385                  $accept = Core::firstHeader($request, 'Accept-Encoding');
 386                  if ($accept) {
 387                      $options[CURLOPT_ENCODING] = $accept;
 388                  } else {
 389                      $options[CURLOPT_ENCODING] = '';
 390                      // Don't let curl send the header over the wire
 391                      $options[CURLOPT_HTTPHEADER][] = 'Accept-Encoding:';
 392                  }
 393                  break;
 394  
 395              case 'save_to':
 396  
 397                  if (is_string($value)) {
 398                      if (!is_dir(dirname($value))) {
 399                          throw new \RuntimeException(sprintf(
 400                              'Directory %s does not exist for save_to value of %s',
 401                              dirname($value),
 402                              $value
 403                          ));
 404                      }
 405                      $value = new LazyOpenStream($value, 'w+');
 406                  }
 407  
 408                  if ($value instanceof StreamInterface) {
 409                      $options[CURLOPT_WRITEFUNCTION] =
 410                          function ($ch, $write) use ($value) {
 411                              return $value->write($write);
 412                          };
 413                  } elseif (is_resource($value)) {
 414                      $options[CURLOPT_FILE] = $value;
 415                  } else {
 416                      throw new \InvalidArgumentException('save_to must be a '
 417                          . 'GuzzleHttp\Stream\StreamInterface or resource');
 418                  }
 419                  break;
 420  
 421              case 'timeout':
 422  
 423                  if (defined('CURLOPT_TIMEOUT_MS')) {
 424                      $options[CURLOPT_TIMEOUT_MS] = $value * 1000;
 425                  } else {
 426                      $options[CURLOPT_TIMEOUT] = $value;
 427                  }
 428                  break;
 429  
 430              case 'connect_timeout':
 431  
 432                  if (defined('CURLOPT_CONNECTTIMEOUT_MS')) {
 433                      $options[CURLOPT_CONNECTTIMEOUT_MS] = $value * 1000;
 434                  } else {
 435                      $options[CURLOPT_CONNECTTIMEOUT] = $value;
 436                  }
 437                  break;
 438  
 439              case 'proxy':
 440  
 441                  if (!is_array($value)) {
 442                      $options[CURLOPT_PROXY] = $value;
 443                  } elseif (isset($request['scheme'])) {
 444                      $scheme = $request['scheme'];
 445                      if (isset($value[$scheme])) {
 446                          $options[CURLOPT_PROXY] = $value[$scheme];
 447                      }
 448                  }
 449                  break;
 450  
 451              case 'cert':
 452  
 453                  if (is_array($value)) {
 454                      $options[CURLOPT_SSLCERTPASSWD] = $value[1];
 455                      $value = $value[0];
 456                  }
 457  
 458                  if (!file_exists($value)) {
 459                      throw new \InvalidArgumentException(
 460                          "SSL certificate not found: {$value}"
 461                      );
 462                  }
 463  
 464                  $options[CURLOPT_SSLCERT] = $value;
 465                  break;
 466  
 467              case 'ssl_key':
 468  
 469                  if (is_array($value)) {
 470                      $options[CURLOPT_SSLKEYPASSWD] = $value[1];
 471                      $value = $value[0];
 472                  }
 473  
 474                  if (!file_exists($value)) {
 475                      throw new \InvalidArgumentException(
 476                          "SSL private key not found: {$value}"
 477                      );
 478                  }
 479  
 480                  $options[CURLOPT_SSLKEY] = $value;
 481                  break;
 482  
 483              case 'progress':
 484  
 485                  if (!is_callable($value)) {
 486                      throw new \InvalidArgumentException(
 487                          'progress client option must be callable'
 488                      );
 489                  }
 490  
 491                  $options[CURLOPT_NOPROGRESS] = false;
 492                  $options[CURLOPT_PROGRESSFUNCTION] =
 493                      function () use ($value) {
 494                          $args = func_get_args();
 495                          // PHP 5.5 pushed the handle onto the start of the args
 496                          if (is_resource($args[0])) {
 497                              array_shift($args);
 498                          }
 499                          call_user_func_array($value, $args);
 500                      };
 501                  break;
 502  
 503              case 'debug':
 504  
 505                  if ($value) {
 506                      $options[CURLOPT_STDERR] = Core::getDebugResource($value);
 507                      $options[CURLOPT_VERBOSE] = true;
 508                  }
 509                  break;
 510              }
 511          }
 512      }
 513  
 514      /**
 515       * This function ensures that a response was set on a transaction. If one
 516       * was not set, then the request is retried if possible. This error
 517       * typically means you are sending a payload, curl encountered a
 518       * "Connection died, retrying a fresh connect" error, tried to rewind the
 519       * stream, and then encountered a "necessary data rewind wasn't possible"
 520       * error, causing the request to be sent through curl_multi_info_read()
 521       * without an error status.
 522       */
 523      private static function retryFailedRewind(
 524          callable $handler,
 525          array $request,
 526          array $response
 527      ) {
 528          // If there is no body, then there is some other kind of issue. This
 529          // is weird and should probably never happen.
 530          if (!isset($request['body'])) {
 531              $response['err_message'] = 'No response was received for a request '
 532                  . 'with no body. This could mean that you are saturating your '
 533                  . 'network.';
 534              return self::createErrorResponse($handler, $request, $response);
 535          }
 536  
 537          if (!Core::rewindBody($request)) {
 538              $response['err_message'] = 'The connection unexpectedly failed '
 539                  . 'without providing an error. The request would have been '
 540                  . 'retried, but attempting to rewind the request body failed.';
 541              return self::createErrorResponse($handler, $request, $response);
 542          }
 543  
 544          // Retry no more than 3 times before giving up.
 545          if (!isset($request['curl']['retries'])) {
 546              $request['curl']['retries'] = 1;
 547          } elseif ($request['curl']['retries'] == 2) {
 548              $response['err_message'] = 'The cURL request was retried 3 times '
 549                  . 'and did no succeed. cURL was unable to rewind the body of '
 550                  . 'the request and subsequent retries resulted in the same '
 551                  . 'error. Turn on the debug option to see what went wrong. '
 552                  . 'See https://bugs.php.net/bug.php?id=47204 for more information.';
 553              return self::createErrorResponse($handler, $request, $response);
 554          } else {
 555              $request['curl']['retries']++;
 556          }
 557  
 558          return $handler($request);
 559      }
 560  }


Generated: Wed Nov 11 20:33:01 2020 Cross-referenced by PHPXref 0.7.1