method = strtoupper($method); $this->curlOptions = new Collection(); $this->setUrl($url); if ($headers) { // Special handling for multi-value headers foreach ($headers as $key => $value) { // Deal with collisions with Host and Authorization if ($key == 'host' || $key == 'Host') { $this->setHeader($key, $value); } elseif ($value instanceof HeaderInterface) { $this->addHeader($key, $value); } else { foreach ((array) $value as $v) { $this->addHeader($key, $v); } } } } $this->setState(self::STATE_NEW); } public function __clone() { if ($this->eventDispatcher) { $this->eventDispatcher = clone $this->eventDispatcher; } $this->curlOptions = clone $this->curlOptions; $this->params = clone $this->params; $this->url = clone $this->url; $this->response = $this->responseBody = null; $this->headers = clone $this->headers; $this->setState(RequestInterface::STATE_NEW); $this->dispatch('request.clone', array('request' => $this)); } /** * Get the HTTP request as a string * * @return string */ public function __toString() { return $this->getRawHeaders() . "\r\n\r\n"; } /** * Default method that will throw exceptions if an unsuccessful response * is received. * * @param Event $event Received * @throws BadResponseException if the response is not successful */ public static function onRequestError(Event $event) { $e = BadResponseException::factory($event['request'], $event['response']); $event['request']->dispatch('request.exception', array( 'request' => $event['request'], 'response' => $event['response'], 'exception' => $e )); throw $e; } public function setClient(ClientInterface $client) { $this->client = $client; return $this; } public function getClient() { return $this->client; } public function getRawHeaders() { $protocolVersion = $this->protocolVersion ?: '1.1'; return trim($this->method . ' ' . $this->getResource()) . ' ' . strtoupper(str_replace('https', 'http', $this->url->getScheme())) . '/' . $protocolVersion . "\r\n" . implode("\r\n", $this->getHeaderLines()); } public function setUrl($url) { if ($url instanceof Url) { $this->url = $url; } else { $this->url = Url::factory($url); } // Update the port and host header $this->setPort($this->url->getPort()); if ($this->url->getUsername() || $this->url->getPassword()) { $this->setAuth($this->url->getUsername(), $this->url->getPassword()); // Remove the auth info from the URL $this->url->setUsername(null); $this->url->setPassword(null); } return $this; } public function send() { if (!$this->client) { throw new RuntimeException('A client must be set on the request'); } return $this->client->send($this); } public function getResponse() { return $this->response; } public function getQuery($asString = false) { return $asString ? (string) $this->url->getQuery() : $this->url->getQuery(); } public function getMethod() { return $this->method; } public function getScheme() { return $this->url->getScheme(); } public function setScheme($scheme) { $this->url->setScheme($scheme); return $this; } public function getHost() { return $this->url->getHost(); } public function setHost($host) { $this->url->setHost($host); $this->setPort($this->url->getPort()); return $this; } public function getProtocolVersion() { return $this->protocolVersion; } public function setProtocolVersion($protocol) { $this->protocolVersion = $protocol; return $this; } public function getPath() { return '/' . ltrim($this->url->getPath(), '/'); } public function setPath($path) { $this->url->setPath($path); return $this; } public function getPort() { return $this->url->getPort(); } public function setPort($port) { $this->url->setPort($port); // Include the port in the Host header if it is not the default port for the scheme of the URL $scheme = $this->url->getScheme(); if (($scheme == 'http' && $port != 80) || ($scheme == 'https' && $port != 443)) { $this->headers['host'] = $this->headerFactory->createHeader('Host', $this->url->getHost() . ':' . $port); } else { $this->headers['host'] = $this->headerFactory->createHeader('Host', $this->url->getHost()); } return $this; } public function getUsername() { return $this->username; } public function getPassword() { return $this->password; } public function setAuth($user, $password = '', $scheme = CURLAUTH_BASIC) { // If we got false or null, disable authentication if (!$user) { $this->password = $this->username = null; $this->removeHeader('Authorization'); $this->getCurlOptions()->remove(CURLOPT_HTTPAUTH); } else { $this->username = $user; $this->password = $password; // Bypass CURL when using basic auth to promote connection reuse if ($scheme == CURLAUTH_BASIC) { $this->getCurlOptions()->remove(CURLOPT_HTTPAUTH); $this->setHeader('Authorization', 'Basic ' . base64_encode($this->username . ':' . $this->password)); } else { $this->getCurlOptions() ->set(CURLOPT_HTTPAUTH, $scheme) ->set(CURLOPT_USERPWD, $this->username . ':' . $this->password); } } return $this; } public function getResource() { $resource = $this->getPath(); if ($query = (string) $this->url->getQuery()) { $resource .= '?' . $query; } return $resource; } public function getUrl($asObject = false) { return $asObject ? clone $this->url : (string) $this->url; } public function getState() { return $this->state; } public function setState($state, array $context = array()) { $oldState = $this->state; $this->state = $state; if ($state == self::STATE_NEW) { $this->response = null; } elseif ($state == self::STATE_COMPLETE && $oldState !== self::STATE_COMPLETE) { $this->processResponse($context); $this->responseBody = null; } return $this; } public function getCurlOptions() { return $this->curlOptions; } public function startResponse(Response $response) { $this->state = self::STATE_TRANSFER; $response->setEffectiveUrl((string) $this->getUrl()); $this->response = $response; return $this; } public function setResponse(Response $response, $queued = false) { $response->setEffectiveUrl((string) $this->url); if ($queued) { $ed = $this->getEventDispatcher(); $ed->addListener('request.before_send', $f = function ($e) use ($response, &$f, $ed) { $e['request']->setResponse($response); $ed->removeListener('request.before_send', $f); }, -9999); } else { $this->response = $response; // If a specific response body is specified, then use it instead of the response's body if ($this->responseBody && !$this->responseBody->getCustomData('default') && !$response->isRedirect()) { $this->getResponseBody()->write((string) $this->response->getBody()); } else { $this->responseBody = $this->response->getBody(); } $this->setState(self::STATE_COMPLETE); } return $this; } public function setResponseBody($body) { // Attempt to open a file for writing if a string was passed if (is_string($body)) { // @codeCoverageIgnoreStart if (!($body = fopen($body, 'w+'))) { throw new InvalidArgumentException('Could not open ' . $body . ' for writing'); } // @codeCoverageIgnoreEnd } $this->responseBody = EntityBody::factory($body); return $this; } public function getResponseBody() { if ($this->responseBody === null) { $this->responseBody = EntityBody::factory()->setCustomData('default', true); } return $this->responseBody; } /** * Determine if the response body is repeatable (readable + seekable) * * @return bool */ public function isResponseBodyRepeatable() { return !$this->responseBody ? true : $this->responseBody->isSeekable() && $this->responseBody->isReadable(); } public function getCookies() { if ($cookie = $this->getHeader('Cookie')) { $data = ParserRegistry::getInstance()->getParser('cookie')->parseCookie($cookie); return $data['cookies']; } return array(); } public function getCookie($name) { $cookies = $this->getCookies(); return isset($cookies[$name]) ? $cookies[$name] : null; } public function addCookie($name, $value) { if (!$this->hasHeader('Cookie')) { $this->setHeader('Cookie', "{$name}={$value}"); } else { $this->getHeader('Cookie')->add("{$name}={$value}"); } // Always use semicolons to separate multiple cookie headers $this->getHeader('Cookie')->setGlue(';'); return $this; } public function removeCookie($name) { if ($cookie = $this->getHeader('Cookie')) { foreach ($cookie as $cookieValue) { if (strpos($cookieValue, $name . '=') === 0) { $cookie->removeValue($cookieValue); } } } return $this; } public function canCache() { // Only GET and HEAD requests can be cached if ($this->method != RequestInterface::GET && $this->method != RequestInterface::HEAD) { return false; } // Never cache requests when using no-store if ($this->getHeader('Cache-Control') && $this->getHeader('Cache-Control')->hasDirective('no-store')) { return false; } return true; } public function setEventDispatcher(EventDispatcherInterface $eventDispatcher) { $this->eventDispatcher = $eventDispatcher; $this->eventDispatcher->addListener('request.error', array(__CLASS__, 'onRequestError'), -255); return $this; } public function getEventDispatcher() { if (!$this->eventDispatcher) { $this->setEventDispatcher(new EventDispatcher()); } return $this->eventDispatcher; } public function dispatch($eventName, array $context = array()) { $context['request'] = $this; $this->getEventDispatcher()->dispatch($eventName, new Event($context)); } public function addSubscriber(EventSubscriberInterface $subscriber) { $this->getEventDispatcher()->addSubscriber($subscriber); return $this; } public function setIsRedirect($isRedirect) { $this->isRedirect = $isRedirect; return $this; } public function isRedirect() { return $this->isRedirect; } /** * {@inheritdoc} * Adds a check for Host header changes */ public function addHeader($header, $value) { parent::addHeader($header, $value); if ($header == 'host' || $header == 'Host') { $this->setHost((string) $this->getHeader('Host')); } return $this; } /** * Get an array containing the request and response for event notifications * * @return array */ protected function getEventArray() { return array( 'request' => $this, 'response' => $this->response ); } /** * Process a received response * * @param array $context Contextual information * @throws RequestException|BadResponseException on unsuccessful responses */ protected function processResponse(array $context = array()) { if (!$this->response) { // If no response, then processResponse shouldn't have been called $e = new RequestException('Error completing request'); $e->setRequest($this); throw $e; } $this->state = self::STATE_COMPLETE; // A request was sent, but we don't know if we'll send more or if the final response will be successful $this->dispatch('request.sent', $this->getEventArray() + $context); // Some response processors will remove the response or reset the state (example: ExponentialBackoffPlugin) if ($this->state == RequestInterface::STATE_COMPLETE) { // The request completed, so the HTTP transaction is complete $this->dispatch('request.complete', $this->getEventArray()); // If the response is bad, allow listeners to modify it or throw exceptions. You can change the response by // modifying the Event object in your listeners or calling setResponse() on the request if ($this->response->isError()) { $event = new Event($this->getEventArray()); $this->getEventDispatcher()->dispatch('request.error', $event); // Allow events of request.error to quietly change the response if ($event['response'] !== $this->response) { $this->response = $event['response']; } } // If a successful response was received, dispatch an event if ($this->response->isSuccessful()) { $this->dispatch('request.success', $this->getEventArray()); } } } }