Overview

Namespaces

  • Contrib
    • Bundle
      • CoverallsBundle
        • Console
        • Entity
      • CoverallsV1Bundle
        • Api
        • Collector
        • Command
        • Config
        • Entity
          • Git
    • Component
      • File
      • Log
      • System
        • Git
  • Guzzle
    • Batch
      • Exception
    • Cache
    • Common
      • Exception
    • Http
      • Curl
      • Exception
      • Message
      • QueryAggregator
    • Inflection
    • Iterator
    • Log
    • Parser
      • Cookie
      • Message
      • UriTemplate
      • Url
    • Plugin
      • Async
      • Backoff
      • Cache
      • Cookie
        • CookieJar
        • Exception
      • CurlAuth
      • ErrorResponse
        • Exception
      • History
      • Log
      • Md5
      • Mock
      • Oauth
    • Service
      • Builder
      • Command
        • Factory
        • LocationVisitor
          • Request
          • Response
      • Description
      • Exception
      • Resource
    • Stream
  • PHP
  • Psr
    • Log
  • Symfony
    • Component
      • Config
        • Definition
          • Builder
          • Exception
        • Exception
        • Loader
        • Resource
        • Util
      • Console
        • Command
        • Formatter
        • Helper
        • Input
        • Output
        • Tester
      • EventDispatcher
        • Debug
      • Finder
        • Adapter
        • Comparator
        • Exception
        • Expression
        • Iterator
        • Shell
      • Stopwatch
      • Yaml
        • Exception

Classes

  • CurlHandle
  • CurlMulti
  • CurlMultiProxy
  • CurlVersion
  • RequestMediator

Interfaces

  • CurlMultiInterface
  • Overview
  • Namespace
  • Class
  • Tree
  • Todo
  1: <?php
  2: 
  3: namespace Guzzle\Http\Curl;
  4: 
  5: use Guzzle\Common\Exception\InvalidArgumentException;
  6: use Guzzle\Common\Exception\RuntimeException;
  7: use Guzzle\Common\Collection;
  8: use Guzzle\Http\Message\EntityEnclosingRequest;
  9: use Guzzle\Http\Message\RequestInterface;
 10: use Guzzle\Parser\ParserRegistry;
 11: use Guzzle\Http\Url;
 12: 
 13: /**
 14:  * Immutable wrapper for a cURL handle
 15:  */
 16: class CurlHandle
 17: {
 18:     const BODY_AS_STRING = 'body_as_string';
 19:     const PROGRESS = 'progress';
 20:     const DEBUG = 'debug';
 21: 
 22:     /**
 23:      * @var Collection Curl options
 24:      */
 25:     protected $options;
 26: 
 27:     /**
 28:      * @var resource Curl resource handle
 29:      */
 30:     protected $handle;
 31: 
 32:     /**
 33:      * @var int CURLE_* error
 34:      */
 35:     protected $errorNo = CURLE_OK;
 36: 
 37:     /**
 38:      * Factory method to create a new curl handle based on an HTTP request.
 39:      *
 40:      * There are some helpful options you can set to enable specific behavior:
 41:      * - debug:    Set to true to enable cURL debug functionality to track the actual headers sent over the wire.
 42:      * - progress: Set to true to enable progress function callbacks.
 43:      *
 44:      * @param RequestInterface $request Request
 45:      *
 46:      * @return CurlHandle
 47:      * @throws RuntimeException
 48:      */
 49:     public static function factory(RequestInterface $request)
 50:     {
 51:         $requestCurlOptions = $request->getCurlOptions();
 52:         $mediator = new RequestMediator($request, $requestCurlOptions->get('emit_io'));
 53:         $tempContentLength = null;
 54:         $method = $request->getMethod();
 55:         $bodyAsString = $requestCurlOptions->get(self::BODY_AS_STRING);
 56: 
 57:         // Array of default cURL options.
 58:         $curlOptions = array(
 59:             CURLOPT_URL            => $request->getUrl(),
 60:             CURLOPT_CONNECTTIMEOUT => 150,
 61:             CURLOPT_RETURNTRANSFER => false,
 62:             CURLOPT_HEADER         => false,
 63:             CURLOPT_PORT           => $request->getPort(),
 64:             CURLOPT_HTTPHEADER     => array(),
 65:             CURLOPT_WRITEFUNCTION  => array($mediator, 'writeResponseBody'),
 66:             CURLOPT_HEADERFUNCTION => array($mediator, 'receiveResponseHeader'),
 67:             CURLOPT_HTTP_VERSION   => $request->getProtocolVersion() === '1.0'
 68:                 ? CURL_HTTP_VERSION_1_0 : CURL_HTTP_VERSION_1_1,
 69:             // Verifies the authenticity of the peer's certificate
 70:             CURLOPT_SSL_VERIFYPEER => 1,
 71:             // Certificate must indicate that the server is the server to which you meant to connect
 72:             CURLOPT_SSL_VERIFYHOST => 2
 73:         );
 74: 
 75:         if (defined('CURLOPT_PROTOCOLS')) {
 76:             // Allow only HTTP and HTTPS protocols
 77:             $curlOptions[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS;
 78:         }
 79: 
 80:         // Add CURLOPT_ENCODING if Accept-Encoding header is provided
 81:         if ($acceptEncodingHeader = $request->getHeader('Accept-Encoding')) {
 82:             $curlOptions[CURLOPT_ENCODING] = (string) $acceptEncodingHeader;
 83:             // Let cURL set the Accept-Encoding header, prevents duplicate values
 84:             $request->removeHeader('Accept-Encoding');
 85:         }
 86: 
 87:         // Enable the progress function if the 'progress' param was set
 88:         if ($requestCurlOptions->get('progress')) {
 89:             $curlOptions[CURLOPT_PROGRESSFUNCTION] = array($mediator, 'progress');
 90:             $curlOptions[CURLOPT_NOPROGRESS] = false;
 91:         }
 92: 
 93:         // Enable curl debug information if the 'debug' param was set
 94:         if ($requestCurlOptions->get('debug')) {
 95:             $curlOptions[CURLOPT_STDERR] = fopen('php://temp', 'r+');
 96:             // @codeCoverageIgnoreStart
 97:             if (false === $curlOptions[CURLOPT_STDERR]) {
 98:                 throw new RuntimeException('Unable to create a stream for CURLOPT_STDERR');
 99:             }
100:             // @codeCoverageIgnoreEnd
101:             $curlOptions[CURLOPT_VERBOSE] = true;
102:         }
103: 
104:         // Specify settings according to the HTTP method
105:         if ($method == 'GET') {
106:             $curlOptions[CURLOPT_HTTPGET] = true;
107:         } elseif ($method == 'HEAD') {
108:             $curlOptions[CURLOPT_NOBODY] = true;
109:             // HEAD requests do not use a write function
110:             unset($curlOptions[CURLOPT_WRITEFUNCTION]);
111:         } elseif (!($request instanceof EntityEnclosingRequest)) {
112:             $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
113:         } else {
114: 
115:             $curlOptions[CURLOPT_CUSTOMREQUEST] = $method;
116: 
117:             // Handle sending raw bodies in a request
118:             if ($request->getBody()) {
119:                 // You can send the body as a string using curl's CURLOPT_POSTFIELDS
120:                 if ($bodyAsString) {
121:                     $curlOptions[CURLOPT_POSTFIELDS] = (string) $request->getBody();
122:                     // Allow curl to add the Content-Length for us to account for the times when
123:                     // POST redirects are followed by GET requests
124:                     if ($tempContentLength = $request->getHeader('Content-Length')) {
125:                         $tempContentLength = (int) (string) $tempContentLength;
126:                     }
127:                     // Remove the curl generated Content-Type header if none was set manually
128:                     if (!$request->hasHeader('Content-Type')) {
129:                         $curlOptions[CURLOPT_HTTPHEADER][] = 'Content-Type:';
130:                     }
131:                 } else {
132:                     $curlOptions[CURLOPT_UPLOAD] = true;
133:                     // Let cURL handle setting the Content-Length header
134:                     if ($tempContentLength = $request->getHeader('Content-Length')) {
135:                         $tempContentLength = (int) (string) $tempContentLength;
136:                         $curlOptions[CURLOPT_INFILESIZE] = $tempContentLength;
137:                     }
138:                     // Add a callback for curl to read data to send with the request only if a body was specified
139:                     $curlOptions[CURLOPT_READFUNCTION] = array($mediator, 'readRequestBody');
140:                     // Attempt to seek to the start of the stream
141:                     $request->getBody()->seek(0);
142:                 }
143: 
144:             } else {
145: 
146:                 // Special handling for POST specific fields and files
147:                 $postFields = false;
148:                 if (count($request->getPostFiles())) {
149:                     $postFields = $request->getPostFields()->useUrlEncoding(false)->urlEncode();
150:                     foreach ($request->getPostFiles() as $key => $data) {
151:                         $prefixKeys = count($data) > 1;
152:                         foreach ($data as $index => $file) {
153:                             // Allow multiple files in the same key
154:                             $fieldKey = $prefixKeys ? "{$key}[{$index}]" : $key;
155:                             $postFields[$fieldKey] = $file->getCurlValue();
156:                         }
157:                     }
158:                 } elseif (count($request->getPostFields())) {
159:                     $postFields = (string) $request->getPostFields()->useUrlEncoding(true);
160:                 }
161: 
162:                 if ($postFields !== false) {
163:                     if ($method == 'POST') {
164:                         unset($curlOptions[CURLOPT_CUSTOMREQUEST]);
165:                         $curlOptions[CURLOPT_POST] = true;
166:                     }
167:                     $curlOptions[CURLOPT_POSTFIELDS] = $postFields;
168:                     $request->removeHeader('Content-Length');
169:                 }
170:             }
171: 
172:             // If the Expect header is not present, prevent curl from adding it
173:             if (!$request->hasHeader('Expect')) {
174:                 $curlOptions[CURLOPT_HTTPHEADER][] = 'Expect:';
175:             }
176:         }
177: 
178:         // If a Content-Length header was specified but we want to allow curl to set one for us
179:         if (null !== $tempContentLength) {
180:             $request->removeHeader('Content-Length');
181:         }
182: 
183:         // Set custom cURL options
184:         foreach ($requestCurlOptions->getAll() as $key => $value) {
185:             if (is_numeric($key)) {
186:                 $curlOptions[$key] = $value;
187:             }
188:         }
189: 
190:         // Do not set an Accept header by default
191:         if (!isset($curlOptions[CURLOPT_ENCODING])) {
192:             $curlOptions[CURLOPT_HTTPHEADER][] = 'Accept:';
193:         }
194: 
195:         // Check if any headers or cURL options are blacklisted
196:         if ($blacklist = $requestCurlOptions->get('blacklist')) {
197:             foreach ($blacklist as $value) {
198:                 if (strpos($value, 'header.') !== 0) {
199:                     unset($curlOptions[$value]);
200:                 } else {
201:                     // Remove headers that may have previously been set but are supposed to be blacklisted
202:                     $key = substr($value, 7);
203:                     $request->removeHeader($key);
204:                     $curlOptions[CURLOPT_HTTPHEADER][] = $key . ':';
205:                 }
206:             }
207:         }
208: 
209:         // Add any custom headers to the request. Empty headers will cause curl to not send the header at all.
210:         foreach ($request->getHeaderLines() as $line) {
211:             $curlOptions[CURLOPT_HTTPHEADER][] = $line;
212:         }
213: 
214:         // Apply the options to a new cURL handle.
215:         $handle = curl_init();
216:         curl_setopt_array($handle, $curlOptions);
217: 
218:         if ($tempContentLength) {
219:             $request->setHeader('Content-Length', $tempContentLength);
220:         }
221: 
222:         $handle = new static($handle, $curlOptions);
223:         $mediator->setCurlHandle($handle);
224: 
225:         return $handle;
226:     }
227: 
228:     /**
229:      * Construct a new CurlHandle object that wraps a cURL handle
230:      *
231:      * @param resource         $handle  Configured cURL handle resource
232:      * @param Collection|array $options Curl options to use with the handle
233:      *
234:      * @throws InvalidArgumentException
235:      */
236:     public function __construct($handle, $options)
237:     {
238:         if (!is_resource($handle)) {
239:             throw new InvalidArgumentException('Invalid handle provided');
240:         }
241:         if (is_array($options)) {
242:             $this->options = new Collection($options);
243:         } elseif ($options instanceof Collection) {
244:             $this->options = $options;
245:         } else {
246:             throw new InvalidArgumentException('Expected array or Collection');
247:         }
248:         $this->handle = $handle;
249:     }
250: 
251:     /**
252:      * Destructor
253:      */
254:     public function __destruct()
255:     {
256:         $this->close();
257:     }
258: 
259:     /**
260:      * Close the curl handle
261:      */
262:     public function close()
263:     {
264:         if (is_resource($this->handle)) {
265:             curl_close($this->handle);
266:         }
267:         $this->handle = null;
268:     }
269: 
270:     /**
271:      * Check if the handle is available and still OK
272:      *
273:      * @return bool
274:      */
275:     public function isAvailable()
276:     {
277:         return is_resource($this->handle);
278:     }
279: 
280:     /**
281:      * Get the last error that occurred on the cURL handle
282:      *
283:      * @return string
284:      */
285:     public function getError()
286:     {
287:         return $this->isAvailable() ? curl_error($this->handle) : '';
288:     }
289: 
290:     /**
291:      * Get the last error number that occurred on the cURL handle
292:      *
293:      * @return int
294:      */
295:     public function getErrorNo()
296:     {
297:         if ($this->errorNo) {
298:             return $this->errorNo;
299:         }
300: 
301:         return $this->isAvailable() ? curl_errno($this->handle) : CURLE_OK;
302:     }
303: 
304:     /**
305:      * Set the curl error number
306:      *
307:      * @param int $error Error number to set
308:      *
309:      * @return CurlHandle
310:      */
311:     public function setErrorNo($error)
312:     {
313:         $this->errorNo = $error;
314: 
315:         return $this;
316:     }
317: 
318:     /**
319:      * Get cURL curl_getinfo data
320:      *
321:      * @param int $option Option to retrieve. Pass null to retrieve all data as an array.
322:      *
323:      * @return array|mixed
324:      */
325:     public function getInfo($option = null)
326:     {
327:         if (!is_resource($this->handle)) {
328:             return null;
329:         }
330: 
331:         if (null !== $option) {
332:             return curl_getinfo($this->handle, $option) ?: null;
333:         }
334: 
335:         return curl_getinfo($this->handle) ?: array();
336:     }
337: 
338:     /**
339:      * Get the stderr output
340:      *
341:      * @param bool $asResource Set to TRUE to get an fopen resource
342:      *
343:      * @return string|resource|null
344:      */
345:     public function getStderr($asResource = false)
346:     {
347:         $stderr = $this->getOptions()->get(CURLOPT_STDERR);
348:         if (!$stderr) {
349:             return null;
350:         }
351: 
352:         if ($asResource) {
353:             return $stderr;
354:         }
355: 
356:         fseek($stderr, 0);
357:         $e = stream_get_contents($stderr);
358:         fseek($stderr, 0, SEEK_END);
359: 
360:         return $e;
361:     }
362: 
363:     /**
364:      * Get the URL that this handle is connecting to
365:      *
366:      * @return Url
367:      */
368:     public function getUrl()
369:     {
370:         return Url::factory($this->options->get(CURLOPT_URL));
371:     }
372: 
373:     /**
374:      * Get the wrapped curl handle
375:      *
376:      * @return resource|null Returns the cURL handle or null if it was closed
377:      */
378:     public function getHandle()
379:     {
380:         return $this->isAvailable() ? $this->handle : null;
381:     }
382: 
383:     /**
384:      * Get the cURL setopt options of the handle. Changing values in the return object will have no effect on the curl
385:      * handle after it is created.
386:      *
387:      * @return Collection
388:      */
389:     public function getOptions()
390:     {
391:         return $this->options;
392:     }
393: 
394:     /**
395:      * Update a request based on the log messages of the CurlHandle
396:      *
397:      * @param RequestInterface $request Request to update
398:      */
399:     public function updateRequestFromTransfer(RequestInterface $request)
400:     {
401:         if (!$request->getResponse()) {
402:             return;
403:         }
404: 
405:         // Update the transfer stats of the response
406:         $request->getResponse()->setInfo($this->getInfo());
407: 
408:         if (!$log = $this->getStderr(true)) {
409:             return;
410:         }
411: 
412:         // Parse the cURL stderr output for outgoing requests
413:         $headers = '';
414:         fseek($log, 0);
415:         while (($line = fgets($log)) !== false) {
416:             if ($line && $line[0] == '>') {
417:                 $headers = substr(trim($line), 2) . "\r\n";
418:                 while (($line = fgets($log)) !== false) {
419:                     if ($line[0] == '*' || $line[0] == '<') {
420:                         break;
421:                     } else {
422:                         $headers .= trim($line) . "\r\n";
423:                     }
424:                 }
425:             }
426:         }
427: 
428:         // Add request headers to the request exactly as they were sent
429:         if ($headers) {
430:             $parsed = ParserRegistry::getInstance()->getParser('message')->parseRequest($headers);
431:             if (!empty($parsed['headers'])) {
432:                 $request->setHeaders(array());
433:                 foreach ($parsed['headers'] as $name => $value) {
434:                     $request->setHeader($name, $value);
435:                 }
436:             }
437:             if (!empty($parsed['version'])) {
438:                 $request->setProtocolVersion($parsed['version']);
439:             }
440:         }
441:     }
442: 
443:     /**
444:      * Parse the config and replace curl.* configurators into the constant based values so it can be used elsewhere
445:      *
446:      * @param array|Collection $config The configuration we want to parse
447:      *
448:      * @return array
449:      */
450:     public static function parseCurlConfig($config)
451:     {
452:         $curlOptions = array();
453:         foreach ($config as $key => $value) {
454:             if (!is_numeric($key) && defined($key)) {
455:                 // Convert constants represented as string to constant int values
456:                 $key = constant($key);
457:             }
458:             if (is_string($value) && defined($value)) {
459:                 $value = constant($value);
460:             }
461:             $curlOptions[$key] = $value;
462:         }
463: 
464:         return $curlOptions;
465:     }
466: }
467: 
php-coveralls API documentation generated by ApiGen 2.8.0