Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00%
0 / 1
55.56%
25 / 45
CRAP
78.46%
204 / 260
Request
0.00%
0 / 1
55.56%
25 / 45
213.60
78.76%
204 / 259
 __construct ( $url = null, $method = self::METHOD_GET, $data = null, $headers = null, $opts = null)
100.00%
1 / 1
1
100.00%
5 / 5
 setUrl ($url)
100.00%
1 / 1
1
100.00%
2 / 2
 getUrl ()
100.00%
1 / 1
1
100.00%
1 / 1
 setMethod ($method)
100.00%
1 / 1
1
100.00%
2 / 2
 getMethod ()
100.00%
1 / 1
1
100.00%
1 / 1
 setHeaders ($headers)
100.00%
1 / 1
1
100.00%
2 / 2
 addHeader ($header)
100.00%
1 / 1
1
100.00%
2 / 2
 getHeaders ()
100.00%
1 / 1
1
100.00%
1 / 1
 setPostBody ($postBody)
100.00%
1 / 1
1
100.00%
2 / 2
 getPostBody ()
100.00%
1 / 1
1
100.00%
1 / 1
 addQueryData ($parms)
0.00%
0 / 1
2
0.00%
0 / 2
 setCurlOptions ($opts)
100.00%
1 / 1
1
100.00%
2 / 2
 addCurlOption ($key, $value)
100.00%
1 / 1
1
100.00%
2 / 2
 getCurlOptions ()
100.00%
1 / 1
1
100.00%
1 / 1
 setUserAgent ($userAgent)
0.00%
0 / 1
2
0.00%
0 / 2
 getUserAgent ()
100.00%
1 / 1
1
100.00%
1 / 1
 setReferrer ($ref)
0.00%
0 / 1
2
0.00%
0 / 2
 setReferer ($ref)
0.00%
0 / 1
2
0.00%
0 / 1
 setConnectTimeout ($ms)
0.00%
0 / 1
6
0.00%
0 / 5
 setTimeout ($ms)
0.00%
0 / 1
2.03
80.00%
4 / 5
 setCredentials ($user, $password, $type = CURLAUTH_ANYSAFE)
0.00%
0 / 1
2
0.00%
0 / 3
 setX509Credentials ($cert, $key, $keypass, $type = 'PEM')
0.00%
0 / 1
2
0.00%
0 / 5
 setCookieJar ($file)
0.00%
0 / 1
6
0.00%
0 / 3
 failIfNot200 ($flag)
0.00%
0 / 1
2
0.00%
0 / 2
 wasSubmitted ()
100.00%
1 / 1
1
100.00%
1 / 1
 throwIfNotSubmitted ()
0.00%
0 / 1
2.50
50.00%
1 / 2
 getResponseHttpCode ()
100.00%
1 / 1
1
100.00%
2 / 2
 setResponseHttpCode ($code)
100.00%
1 / 1
1
100.00%
2 / 2
 getResponseHeaders ()
100.00%
1 / 1
1
100.00%
2 / 2
 getResponseHeader ($name)
0.00%
0 / 1
6
0.00%
0 / 4
 setResponseHeaders ($headers)
100.00%
1 / 1
1
100.00%
2 / 2
 getResponseBody ()
100.00%
1 / 1
1
100.00%
2 / 2
 setResponseBody ($body)
100.00%
1 / 1
1
100.00%
2 / 2
 getResponseCurlInfo ()
0.00%
0 / 1
2
0.00%
0 / 2
 setResponseCurlInfo ($info)
100.00%
1 / 1
1
100.00%
2 / 2
 submit ($failIfNot200 = null)
0.00%
0 / 1
2.01
85.71%
6 / 7
 createCurlRequest ()
100.00%
1 / 1
12
100.00%
38 / 38
 processCurlResponse ($ch, $errCode, $errMsg, $rawResp)
0.00%
0 / 1
8.16
86.49%
32 / 37
 validateResponse ($failIfNot200 = null)
0.00%
0 / 1
26.42
91.49%
43 / 47
 parallelSubmit ($requests)
0.00%
0 / 1
12.05
92.86%
26 / 28
 handleMultiResponse ($info, $mh, &$handles)
100.00%
1 / 1
2
100.00%
10 / 10
 get ($url, $parms = null, $options = null)
0.00%
0 / 1
2
0.00%
0 / 3
 post ($url, $parms, $options = null)
0.00%
0 / 1
2
0.00%
0 / 4
 postContent ( $url, $content, $contentType = 'text/xml', $options = null)
100.00%
1 / 1
1
100.00%
4 / 4
 postMultipart ($url, $parms, $options = null)
0.00%
0 / 1
2
0.00%
0 / 3
<?php
/**
 * @package Moar\Net\Http
 */
namespace Moar\Net\Http;
// ensure that curl error constants are available
Util::ensureCurlErrorConstants();
/**
 * HTTP request handler that attempts to make using cURL easy.
 *
 * Static convenience methods are provided to performing typical requests such
 * as GET and POST. Several variations of POST are available for
 * "application/x-www-form-urlencoded", "multipart/form-data" and raw body
 * encodings.
 *
 * More complicated requests can be configured via direct use of the class.
 * Helper methods are provided for common operations such as authenticating
 * with a a username/password pair or an x509 certificate.
 *
 * Requests are sent by calling the submit() method directly or by
 * passing an array of prepared requests to the static parallelSubmit() method.
 * Either method of executing a request will result in the provided
 * Request being updated to contain response codes, headers and body data
 * returned by the server processing the URL.
 *
 * @package Moar\Net\Http
 */
class Request {
  /**
   * Default user-agent string.
   * @var string
   */
  const DEFAULT_USERAGENT = 'Mozilla/5.0 (compatible; Moar-Net-Http-Request)';
  /**
   * Default maximum redirects.
   * @var int
   */
  const DEFAULT_MAXREDIRS = 10;
  /**
   * HTTP GET.
   * @var string
   */
  const METHOD_GET = 'GET';
  /**
   * HTTP POST.
   * @var string
   */
  const METHOD_POST = 'POST';
  /**
   * Default curlopts.
   * @var array
   */
  protected static $defaultCurlOpts = array(
      CURLOPT_SSL_VERIFYPEER    => false,
      CURLOPT_SSL_VERIFYHOST    => false,
      CURLINFO_SSL_VERIFYRESULT => false,
      CURLOPT_FOLLOWLOCATION    => true,
      CURLOPT_MAXREDIRS         => self::DEFAULT_MAXREDIRS,
      CURLOPT_HTTP_VERSION      => CURL_HTTP_VERSION_1_1,
      CURLOPT_ENCODING          => '',
      CURLOPT_CONNECTTIMEOUT    => 3, // 3 seconds
      CURLOPT_TIMEOUT           => 5, // 5 seconds
    );
  /**
   * URL to request.
   * @var string
   */
  protected $url;
  /**
   * HTTP request method.
   * @var string
   */
  protected $method;
  /**
   * HTTP request headers.
   * @var array
   */
  protected $headers = array();
  /**
   * POST body.
   * @var string
   */
  protected $postBody;
  /**
   * User-Agent header value.
   * @var string
   */
  protected $userAgent = self::DEFAULT_USERAGENT;
  /**
   * Curl options.
   * @var array
   */
  protected $curlOptions = array();
  /**
   * Default behavior for non-200 status responses.
   * @var bool
   */
  protected $defaultFailIfNot200 = true;
  /**
   * Response code.
   * @var int
   */
  protected $responseHttpCode;
  /**
   * Response headers.
   * @var array
   */
  protected $responseHeaders;
  /**
   * Response body.
   * @var string
   */
  protected $responseBody;
  /**
   * Curl response information.
   * @var array
   */
  protected $responseCurlInfo;
  /**
   * Curl error status.
   * @var int
   */
  protected $responseCurlErr;
  /**
   * Curl error message.
   * @var string
   */
  protected $responseCurlErrMessage;
  /**
   * Constructor.
   *
   * @param string $url URL to request
   * @param string $method HTTP request verb
   * @param mixed  $data Data to send with request. Either a URL-encoded
   *    string or an array of key=>value pairs.
   * @param array  $headers Custom headers to set on request
   * @param array  $opts Curl configuration options
   */
  public function __construct (
      $url = null, $method = self::METHOD_GET, $data = null, $headers = null,
      $opts = null) {
    $this->setUrl($url);
    $this->setMethod($method);
    $this->setHeaders($headers);
    $this->setPostBody($data);
    $this->setCurlOptions($opts);
  } //end __construct
  /**
   * Set the url for this request.
   * @param string $url URL to request
   * @return Request Self, for message chaining
   */
  public function setUrl ($url) {
    $this->url = $url;
    return $this;
  }
  /**
   * Get the url for this request.
   * @return string URL of request
   */
  public function getUrl () {
    return $this->url;
  }
  /**
   * Set the method for this request.
   * @param string $method HTTP request verb
   * @return Request Self, for message chaining
   */
  public function setMethod ($method) {
    $this->method = $method;
    return $this;
  }
  /**
   * Get the method for this request.
   * @return string HTTP request verb
   */
  public function getMethod () {
    return $this->method;
  }
  /**
   * Set the headers for this request.
   * @param array $headers Custom headers to set on request
   * @return Request Self, for message chaining
   */
  public function setHeaders ($headers) {
    $this->headers = (array) $headers;
    return $this;
  }
  /**
   * Add a header for this request.
   * @param string $header Header to send with request
   * @return Request Self, for message chaining
   */
  public function addHeader ($header) {
    $this->headers[] = $header;
    return $this;
  }
  /**
   * Get the custom headers for this request.
   * @return array Collection of custom headers
   */
  public function getHeaders () {
    return $this->headers;
  }
  /**
   * Set the postBody for this request.
   * @param mixed $postBody Either literal payload for request or array of
   *    key=>value pairs to encode as "multipart/form-data" on submission.
   * @return Request Self, for message chaining
   */
  public function setPostBody ($postBody) {
    $this->postBody = $postBody;
    return $this;
  }
  /**
   * Get the postBody for this request.
   * @return string HTTP request verb
   */
  public function getPostBody () {
    return $this->postBody;
  }
  /**
   * Append a query string to the current URL.
   *
   * @param string|array $parms Parameters to add as query string to url
   * @return Request Self, for message chaining
   */
  public function addQueryData ($parms) {
    $this->setUrl(Util::addQueryData($this->getUrl(), $parms));
    return $this;
  }
  /**
   * Set cURL options for this request.
   * @param array $opts Curl options
   * @return Request Self, for message chaining
   */
  public function setCurlOptions ($opts) {
    $this->curlOptions = (array) $opts;
    return $this;
  }
  /**
   * Add a cURL option for this request.
   * @param mixed $key Curl option identifier
   * @param mixed $value Curl option value
   * @return Request Self, for message chaining
   */
  public function addCurlOption ($key, $value) {
    $this->curlOptions[$key] = $value;
    return $this;
  }
  /**
   * Get the cURL options for this request.
   * @return array Curl configuration options
   */
  public function getCurlOptions () {
    return $this->curlOptions;
  }
  /**
   * Set the value of the User-Agent header for this request.
   * @param string $userAgent The User-Agent
   * @return Request Self, for message chaining
   */
  public function setUserAgent ($userAgent) {
    $this->userAgent = $userAgent;
    return $this;
  }
  /**
   * Get the value of the User-Agent header for this request.
   * @return string The User-Agent
   */
  public function getUserAgent () {
    return $this->userAgent;
  }
  /**
   * Set the referring URL that this request is realted to.
   * @param string $ref URL to report to server in "Referer" header
   * @return Request Self, for message chaining
   */
  public function setReferrer ($ref) {
    $this->addCurlOption(CURLOPT_REFERER, $ref);
    return $this;
  }
  /**
   * Synonym for setReferrer().
   * @param string $ref URL to report to server in "Referer" header
   * @return Request Self, for message chaining
   * @see setReferrer()
   */
  public function setReferer ($ref) {
    return $this->setReferrer($ref);
  }
  /**
   * Set HTTP connect timeout (in milliseconds).
   * @param int $ms Connect timeout in millseconds
   * @return Request Self, for message chaining
   */
  public function setConnectTimeout ($ms) {
    if (defined('CURLOPT_CONNECTTIMEOUT_MS')) {
      $this->addCurlOption(CURLOPT_CONNECTTIMEOUT_MS, $ms);
    } else {
      // older versions of php/libcurl don't have the sub-second timeout
      // functionality. Convert to nearest whole seconds not less than 1.
      $this->addCurlOption(CURLOPT_CONNECTTIMEOUT, max(1, round($ms / 1000)));
    }
    return $this;
  } //end setConnectTimeout
  /**
   * Set HTTP timeout (in milliseconds).
   * @param int $ms Timeout in millseconds
   * @return Request Self, for message chaining
   */
  public function setTimeout ($ms) {
    if (defined('CURLOPT_TIMEOUT_MS')) {
      $this->addCurlOption(CURLOPT_TIMEOUT_MS, $ms);
    } else {
      // older versions of php/libcurl don't have the sub-second timeout
      // functionality. Convert to nearest whole seconds not less than 1.
      $this->addCurlOption(CURLOPT_TIMEOUT, max(1, round($ms / 1000)));
    }
    return $this;
  } //end setTimeout
  /**
   * Set credentials for authenticating to remote host.
   *
   * @param string $user Username
   * @param string $password Password
   * @param int    $type Auth type to attempt. See CURLOPT_HTTPAUTH section of
   *    curl_setopt page at php.net for options.
   * @return Request Self, for message chaining
   */
  public function setCredentials ($user, $password, $type = CURLAUTH_ANYSAFE) {
    $this->addCurlOption(CURLOPT_HTTPAUTH, $type);
    $this->addCurlOption(CURLOPT_USERPWD, "{$user}:{$password}");
    return $this;
  } //end setCredentials
  /**
   * Set x509 credentials for authenticating to remote host.
   *
   * @param string $cert Path to x509 certificate
   * @param string $key Patch to x509 private key
   * @param string $keypass Passphrase to decrypt private key
   * @param string $type Certificate encoding (PEM|DER|ENG)
   * @return Request Self, for message chaining
   */
  public function setX509Credentials ($cert, $key, $keypass, $type = 'PEM') {
    $this->addCurlOption(CURLOPT_SSLCERTTYPE, $type);
    $this->addCurlOption(CURLOPT_SSLCERT, $cert);
    $this->addCurlOption(CURLOPT_SSLKEY, $key);
    $this->addCurlOption(CURLOPT_SSLKEYPASSWD, $keypass);
    return $this;
  }
  /**
   * Read and store cookies in the provided file.
   *
   * @param string $file Path to cookie storage file
   * @return Request Self, for message chaining
   */
  public function setCookieJar ($file) {
    if (null !== $file) {
      $this->addCurlOption(CURLOPT_COOKIEFILE, $file); //read from file
      $this->addCurlOption(CURLOPT_COOKIEJAR, $file); //write to file
    }
    return $this;
  }
  /**
   * Set the default behavior when a request returns a non-200 status code.
   *
   * @param bool $flag True to throw an exception, false otherwise
   * @return Request Self, for message chaining
   * @see getResponseHttpCode()
   */
  public function failIfNot200 ($flag) {
    $this->defaultFailIfNot200 = (bool) $flag;
    return $this;
  }
  /**
   * Check to see if this request has been submitted yet.
   *
   * @return bool True if request has been submitted, false otherwise.
   */
  public function wasSubmitted () {
    return null !== $this->responseCurlErr;
  }
  /**
   * Throw a \RuntimeException if this request has not been submitted
   * yet.
   *
   * @return void
   * @throws \RuntimeException If request has not been submitted.
   */
  protected function throwIfNotSubmitted () {
    if (!$this->wasSubmitted()) {
      throw new \RuntimeException('Request not submitted.');
    }
  } //end throwIfNotSubmitted
  /**
   * Get the HTTP response code sent by the server.
   * @return int HTTP response code
   * @throws \RuntimeException If request has not been submitted.
   */
  public function getResponseHttpCode () {
    $this->throwIfNotSubmitted();
    return $this->responseHttpCode;
  }
  /**
   * Set the HTTP response code sent by the server.
   * @param int $code HTTP response code
   * @return Request Self, for message chaining
   */
  protected function setResponseHttpCode ($code) {
    $this->responseHttpCode = $code;
    return $this;
  }
  /**
   * Get the HTTP response headers sent by the server.
   *
   * Keys in the header array are the header names. The value is either the
   * raw header contents or an array of raw header contents if that particular
   * header appeared more than once in the response.
   *
   * @return array HTTP response headers
   * @throws \RuntimeException If request has not been submitted.
   */
  public function getResponseHeaders () {
    $this->throwIfNotSubmitted();
    return $this->responseHeaders;
  }
  /**
   * Get a particular HTTP response header sent by the server.
   *
   * @param string $name Header name
   * @return mixed Header value or null if not found. Value may be an array if
   *    the named header occured more than once in the server's response.
   * @throws \RuntimeException If request has not been submitted.
   */
  public function getResponseHeader ($name) {
    $this->throwIfNotSubmitted();
    if (isset($this->responseHeaders[$name])) {
      return $this->responseHeaders[$name];
    } else {
      return null;
    }
  } //end getResponseHeader
  /**
   * Set the HTTP response headers sent by the server.
   * @param array $headers HTTP response headers
   * @return Request Self, for message chaining
   */
  protected function setResponseHeaders ($headers) {
    $this->responseHeaders = $headers;
    return $this;
  }
  /**
   * Get the HTTP response body sent by the server.
   * @return string HTTP response body
   * @throws \RuntimeException If request has not been submitted.
   */
  public function getResponseBody () {
    $this->throwIfNotSubmitted();
    return $this->responseBody;
  }
  /**
   * Set the HTTP response body sent by the server.
   * @param array $body HTTP response body
   * @return Request Self, for message chaining
   */
  protected function setResponseBody ($body) {
    $this->responseBody = $body;
    return $this;
  }
  /**
   * Get the cURL response information.
   * @return string HTTP response body
   * @throws \RuntimeException If request has not been submitted.
   */
  public function getResponseCurlInfo () {
    $this->throwIfNotSubmitted();
    return $this->responseCurlInfo;
  }
  /**
   * Set the cURL response information.
   * @param array $info Curl info
   * @return Request Self, for message chaining
   */
  protected function setResponseCurlInfo ($info) {
    $this->responseCurlInfo = $info;
    return $this;
  }
  /**
   * Submit this request.
   *
   * This request will be updated with the results of the request which can
   * then be retrieved using getResponseHttpCode() and releated methods.
   *
   * @param bool $failIfNot200 Throw an exception if a non-200 status was sent?
   * @return Request Request with response data populated
   * @throws Exception On cURL failure
   */
  public function submit ($failIfNot200 = null) {
    if ($this->wasSubmitted()) {
      throw new Exception("Request be reused!");
    }
    $ch = $this->createCurlRequest();
    $raw = curl_exec($ch);
    $this->processCurlResponse($ch, curl_errno($ch), curl_error($ch), $raw);
    $this->validateResponse($failIfNot200);
    return $this;
  }
  /**
   * Prepare a cURL handle for this request.
   * @return resource Curl handle ready to be submitted
   */
  protected function createCurlRequest () {
    // merge any custom cURL options into the default set
    $curlOpts = Util::mergeCurlOptions(
        self::$defaultCurlOpts, $this->getCurlOptions());
    // set basic options that we always want to use
    $curlOpts[CURLOPT_RETURNTRANSFER] = true;
    $curlOpts[CURLOPT_HEADER] = true;
    $curlOpts[CURLOPT_FAILONERROR] = false;
    if (defined('CURLINFO_HEADER_OUT')) {
      $curlOpts[CURLINFO_HEADER_OUT] = true;
    }
    // timeouts less than 1 sec fail unless we disable signals
    // see http://www.php.net/manual/en/function.curl-setopt.php#104597
    if (defined('CURLOPT_TIMEOUT_MS') ||
        defined('CURLOPT_CONNECTTIMEOUT_MS')) {
      if ((isset($curlOpts[CURLOPT_TIMEOUT_MS]) &&
          $curlOpts[CURLOPT_TIMEOUT_MS] < 1000) ||
          (isset($curlOpts[CURLOPT_CONNECTTIMEOUT_MS]) &&
          $curlOpts[CURLOPT_CONNECTTIMEOUT_MS] < 1000)) {
        $curlOpts[CURLOPT_NOSIGNAL] = true;
      }
    }
    // prepare the request
    $curlOpts[CURLOPT_URL] = $this->url;
    if (self::METHOD_POST == $this->getMethod()) {
      // using CURLOPT_CUSTOMREQUEST changes the behavior for POST
      // this change forces strict RFC2616:10.3.3 compliance which doesn't
      // work too well with many internet sites.
      $curlOpts[CURLOPT_POST] = true;
    } else {
      $curlOpts[CURLOPT_CUSTOMREQUEST] = $this->getMethod();
    }
    $curlOpts[CURLOPT_USERAGENT] = $this->getUserAgent();
    if ($this->getPostBody()) {
      // add post payload
      $pBody = $this->getPostBody();
      if (!is_array($pBody)) {
        // caller supplied a URI-encoded payload, so make sure we set the
        // content-length header
        $len = mb_strlen($pBody, 'latin1');
        $this->addHeader("Content-Length: {$len}");
      }
      $curlOpts[CURLOPT_POSTFIELDS] = $pBody;
    }
    if ($this->getHeaders()) {
      // add custom headers
      $curlOpts[CURLOPT_HTTPHEADER] = $this->getHeaders();
    }
    // remember the options we used. Might be handy for debugging.
    $this->setCurlOptions($curlOpts);
    // create curl resource
    $ch = curl_init();
    // apply options
    curl_setopt_array($ch, $curlOpts);
    return $ch;
  } //end createCurlRequest
  /**
   * Process an submitted cURL response.
   * @param resource $ch Curl handle
   * @param int      $errCode Curl error code
   * @param string   $errMsg Curl error message
   * @param string   $rawResp Raw response
   * @return Request Request with response data populated
   */
  protected function processCurlResponse ($ch, $errCode, $errMsg, $rawResp) {
    // check error codes
    $this->responseCurlErr = $errCode;
    $this->responseCurlErrMessage = $errMsg;
    if (CURLE_OK == $errCode) {
      $info = curl_getinfo($ch);
      $hdrSize = $info['header_size'];
      $respCode = (int) $info['http_code'];
      // parse the raw response
      $rawHeaders = mb_substr($rawResp, 0, $hdrSize, 'latin1');
      $respBody =  mb_substr(
          $rawResp, $hdrSize, mb_strlen($rawResp, 'latin1'), 'latin1');
      // parse response headers
      if ($info['redirect_count'] > 0) {
        // discard redirect headers
        // TODO: there may be useful info in here that we want to preserve
        $headerChunks = explode("\r\n\r\n", $rawHeaders);
        $rawHeaders = $headerChunks[$info['redirect_count']];
      }
      $respHeaderParts = explode("\r\n", $rawHeaders);
      $respHeaders = array();
      foreach ($respHeaderParts as $header) {
        if ($header) {
          $parts = explode(': ', $header, 2);
          $name = $parts[0];
          $value = '';
          if (count($parts) == 2) {
            $value = $parts[1];
          }
          if (isset($respHeaders[$name])) {
            if (!is_array($respHeaders[$name])) {
              // convert single value to collection
              $respHeaders[$name] = array($respHeaders[$name]);
            }
            $respHeaders[$name][] = $value;
          } else {
            $respHeaders[$name] = $value;
          }
        }
      } //end foreach $header
      // fill in request with response
      $this->setResponseCurlInfo($info);
      $this->setResponseHttpCode($respCode);
      $this->setResponseHeaders($respHeaders);
      $this->setResponseBody($respBody);
    } //end if CURLE_OK
    curl_close($ch);
    return $this;
  } //end processCurlResponse
  /**
   * Check the cURL response code and throw an exception if it is an error.
   * @param bool $failIfNot200 Throw an exception if a non-200 status was sent?
   * @return void
   * @throws Exception On cURL failure
   */
  public function validateResponse ($failIfNot200 = null) {
    if (null === $failIfNot200) {
      $failIfNot200 = $this->defaultFailIfNot200;
    }
    if (CURLE_OK != $this->responseCurlErr) {
      $exClazz = 'Moar\Net\Http\Exception';
      switch ($this->responseCurlErr) {
        case CURLE_UNSUPPORTED_PROTOCOL:
        case CURLE_URL_MALFORMAT:
          $exClazz = 'Moar\Net\Http\BadUrlException';
          break;
        case CURLE_COULDNT_RESOLVE_HOST:
          $exClazz = 'Moar\Net\Http\DnsFailureException';
          break;
        case CURLE_COULDNT_CONNECT:
          $exClazz = 'Moar\Net\Http\ConnectFailedException';
          break;
        case CURLE_HTTP_RETURNED_ERROR:
          $exClazz = 'Moar\Net\Http\StatusCodeException';
          break;
        case CURLE_OPERATION_TIMEDOUT:
          $exClazz = 'Moar\Net\Http\TimeoutException';
          break;
        case CURLE_PEER_FAILED_VERIFICATION:
        case CURLE_SSL_CACERT:
        case CURLE_SSL_CACERT_BADFILE:
        case CURLE_SSL_CERTPROBLEM:
        case CURLE_SSL_CERTPROBLEM:
        case CURLE_SSL_CIPHER:
        case CURLE_SSL_CONNECT_ERROR:
        case CURLE_SSL_CRL_BADFILE:
        case CURLE_SSL_ENGINE_INITFAILED:
        case CURLE_SSL_ENGINE_NOTFOUND:
        case CURLE_SSL_ENGINE_SETFAILED:
        case CURLE_SSL_ISSUER_ERROR:
        case CURLE_SSL_SHUTDOWN_FAILED:
        case CURLE_USE_SSL_FAILED:
          $exClazz = 'Moar\Net\Http\SslException';
          break;
      } //end switch
      throw new $exClazz(
          $this->responseCurlErrMessage, $this->responseCurlErr, $this);
    } //end if !ok
    if ($failIfNot200) {
      $code = $this->getResponseHttpCode();
      if ($code < 200 || $code > 299) {
        throw new StatusCodeException(
            "HTTP Error: ({$code}) from {$this->url}",
            CURLE_HTTP_RETURNED_ERROR, $this);
      }
    }
  } //end validateResponse
  /**
   * Submit a group of requests in parallel.
   *
   * Uses the curl_multi_exec engine to fire off several requests in parallel
   * and waits for all responses to finish before returing the collective
   * results.
   *
   * @param array $requests List of requests to submit
   * @return array List of submitted requests
   * @throws Exception If a fatal multi error occurs.
   */
  public static function parallelSubmit ($requests) {
    $handles = array();
    $mh = curl_multi_init();
    // make a curl handle for each request
    foreach ($requests as $req) {
      $ch = $req->createCurlRequest();
      $handles[(string) $ch] = $req;
      curl_multi_add_handle($mh, $ch);
    }
    $reqsRunning = 0;
    // fire off initial requests
    do {
      $status = curl_multi_exec($mh, $reqsRunning);
    } while (CURLM_CALL_MULTI_PERFORM == $status);
    // process the requests
    while ($reqsRunning && CURLM_OK == $status) {
      // wait to be woken up by network activity
      $selectReady = curl_multi_select($mh);
      if ($selectReady > 0) {
        // one or more requests are finished
        while ($info = curl_multi_info_read($mh)) {
          self::handleMultiResponse($info, $mh, $handles);
        }
      } //end if selectReady
      if (-1 != $selectReady) {
        // continue processing
        do {
          $status = curl_multi_exec($mh, $reqsRunning);
        } while (CURLM_CALL_MULTI_PERFORM == $status);
      }
    } //end while requests to process
    // check for critical failure
    if (CURLM_OK != $status) {
      throw new Exception(
          "Fatal error [{$status}] processing multiple requests", $status);
    }
    // any remaining results should be ready now
    while ($handles && ($info = curl_multi_info_read($mh))) {
      self::handleMultiResponse($info, $mh, $handles);
    }
    curl_multi_close($mh);
    return $requests;
  } //end parallelSubmit
  /**
   * Handle a curl_multi_info_read() response message.
   *
   * @param array $info Status message from Curl
   * @param resource $mh Curl multi handle
   * @param array &$handles List of Curl handles still outstanding
   * @return void
   * @see self::parallelSubmit()
   */
  protected static function handleMultiResponse ($info, $mh, &$handles) {
    $ch = $info['handle'];
    $req = $handles[(string) $ch];
    $rawResp = null;
    if (CURLE_OK == $info['result']) {
      // read the response from the handle
      $rawResp = curl_multi_getcontent($ch);
    }
    $req->processCurlResponse(
        $ch, $info['result'], curl_error($ch), $rawResp);
    // remove this handle from the queue
    curl_multi_remove_handle($mh, $ch);
    unset($handles[(string) $ch]);
  } //end handleMultiResponse
  /**
   * Convenience method to perform a GET request.
   *
   * Provided parameters will be "application/x-www-form-urlencoded" encoded
   * and appended to the provided URL.
   *
   * @param string $url URL to get (eg https://www.keynetics.com/page.php)
   * @param array $parms Array of key => value pairs to send
   * @param array $options Array of extra options to pass to cURL
   * @return Request Submitted request
   * @throws Exception On failure
   */
  public static function get ($url, $parms = null, $options = null) {
    $url = Util::addQueryData($url, $parms);
    $r = new Request($url, self::METHOD_GET, null, null, $options);
    return $r->submit();
  } //end get
  /**
   * Convenience method to POST data to a URL and retrieve the results.
   *
   * Provided parms will be encoded as "application/x-www-form-urlencoded"
   * data before being sent.
   *
   * @param string $url Full URL to post to (eg
   *    https://www.keynetics.com/page.php)
   * @param array $parms Array of key => value pairs to post
   * @param array $options Array of extra options to pass to cURL
   * @return Request Submitted request
   * @throws Exception On failure
   */
  public static function post ($url, $parms, $options = null) {
    $encParms = Util::urlEncode($parms);
    $r = new Request(
        $url, self::METHOD_POST, $encParms, null, $options);
    return $r->submit();
  } //end post
  /**
   * Convenience method to POST content in the form of a raw string to a URL.
   *
   * This is useful for manually constructed SOAP requests and other document
   * body type operations. Default content type is text/xml.
   *
   * @param string $url Full URL to post to (eg
   *    https://www.keynetics.com/page.php)
   * @param string $content Raw HTTP request body to be posted
   * @param string $contentType Value of the HTTP Content-Type header
   * @param array $options Array of extra options to pass to cURL
   * @return Request Submitted request
   * @throws Exception On failure
   */
  public static function postContent (
      $url, $content, $contentType = 'text/xml', $options = null) {
    $headers = array("Content-type: {$contentType}");
    $r = new Request(
        $url, self::METHOD_POST, $content, $headers, $options);
    return $r->submit();
  } //end postContent
  /**
   * Convenience method to POST data to a URL and retrieve the results.
   *
   * Provided parms will be encoded as "multipart/form-data" data before being
   * sent.
   *
   * @param string $url Full URL to post to (eg
   *    https://www.keynetics.com/page.php)
   * @param array $parms Array of key => value pairs to post
   * @param array $options Array of extra options to pass to cURL
   * @return Request Submitted request
   * @throws Exception On failure
   */
  public static function postMultipart ($url, $parms, $options = null) {
    $r = new Request(
        $url, self::METHOD_POST, $parms, null, $options);
    return $r->submit();
  } //end postMultipart
} //end Request