8 * Copyright (c) 2009-2013 Nicholas J Humfrey. All rights reserved.
9 * Copyright (c) 2005-2009 Zend Technologies USA Inc.
11 * Redistribution and use in source and binary forms, with or without
12 * modification, are permitted provided that the following conditions are met:
13 * 1. Redistributions of source code must retain the above copyright
14 * notice, this list of conditions and the following disclaimer.
15 * 2. Redistributions in binary form must reproduce the above copyright notice,
16 * this list of conditions and the following disclaimer in the documentation
17 * and/or other materials provided with the distribution.
18 * 3. The name of the author 'Nicholas J Humfrey" may be used to endorse or
19 * promote products derived from this software without specific prior
22 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 * POSSIBILITY OF SUCH DAMAGE.
35 * @copyright Copyright (c) 2009-2013 Nicholas J Humfrey
36 * Copyright (c) 2005-2009 Zend Technologies USA Inc.
37 * @license http://www.opensource.org/licenses/bsd-license.php
41 * This class is an implemetation of an HTTP client in PHP.
42 * It supports basic HTTP 1.0 and 1.1 requests. For a more complete
43 * implementation try Zend_Http_Client.
46 * @copyright Copyright (c) 2009-2013 Nicholas J Humfrey
47 * @license http://www.opensource.org/licenses/bsd-license.php
49 class EasyRdf_Http_Client
52 * Configuration array, set using the constructor or using ::setConfig()
56 private $config = array(
58 'useragent' => 'EasyRdf_Http_Client',
70 * Associative array of request headers
74 private $headers = array();
81 private $method = 'GET';
84 * Associative array of GET parameters
88 private $paramsGet = array();
91 * The raw post data to send. Could be set by setRawData($data).
95 private $rawPostData = null;
102 private $redirectCounter = 0;
105 * Constructor method. Will create a new HTTP client. Accepts the target
106 * URL and optionally configuration array.
109 * @param array $config Configuration key-value pairs.
111 public function __construct($uri = null, $config = null)
116 if ($config !== null) {
117 $this->setConfig($config);
122 * Set the URI for the next request
125 * @return EasyRdf_Http_Client
127 public function setUri($uri)
129 if (!is_string($uri)) {
133 if (!preg_match('/^http(s?):/', $uri)) {
134 throw new InvalidArgumentException(
135 "EasyRdf_Http_Client only supports the 'http' and 'https' schemes."
145 * Get the URI for the next request
149 public function getUri($asString = true)
155 * Set configuration parameters for this HTTP client
157 * @param array $config
158 * @return EasyRdf_Http_Client
159 * @throws InvalidArgumentException
161 public function setConfig($config = array())
163 if ($config == null or !is_array($config)) {
164 throw new InvalidArgumentException(
165 "\$config should be an array and cannot be null"
169 foreach ($config as $k => $v) {
170 $this->config[strtolower($k)] = $v;
177 * Set a request header
179 * @param string $name Header name (e.g. 'Accept')
180 * @param string $value Header value or null
181 * @return EasyRdf_Http_Client
183 public function setHeaders($name, $value = null)
185 $normalizedName = strtolower($name);
187 // If $value is null or false, unset the header
188 if ($value === null || $value === false) {
189 unset($this->headers[$normalizedName]);
191 // Else, set the header
192 $this->headers[$normalizedName] = array($name, $value);
199 * Set the next request's method
201 * Validated the passed method and sets it.
203 * @param string $method
204 * @return EasyRdf_Http_Client
205 * @throws InvalidArgumentException
207 public function setMethod($method)
209 if (!is_string($method) or !preg_match('/^[A-Z]+$/', $method)) {
210 throw new InvalidArgumentException("Invalid HTTP request method.");
213 $this->method = $method;
219 * Get the method for the next request
223 public function getMethod()
225 return $this->method;
229 * Get the value of a specific header
231 * Note that if the header has more than one value, an array
235 * @return string|array|null The header value or null if it is not set
237 public function getHeader($key)
239 $key = strtolower($key);
240 if (isset($this->headers[$key])) {
241 return $this->headers[$key][1];
248 * Set a GET parameter for the request.
250 * @param string $name
251 * @param string $value
252 * @return EasyRdf_Http_Client
254 public function setParameterGet($name, $value = null)
256 if ($value === null) {
257 if (isset($this->paramsGet[$name])) {
258 unset($this->paramsGet[$name]);
261 $this->paramsGet[$name] = $value;
268 * Get a GET parameter for the request.
270 * @param string $name
271 * @return string value
273 public function getParameterGet($name)
275 if (isset($this->paramsGet[$name])) {
276 return $this->paramsGet[$name];
283 * Get all the GET parameters
287 public function getParametersGet()
289 return $this->paramsGet;
293 * Get the number of redirections done on the last request
297 public function getRedirectionsCount()
299 return $this->redirectCounter;
303 * Set the raw (already encoded) POST data.
305 * This function is here for two reasons:
306 * 1. For advanced user who would like to set their own data, already encoded
307 * 2. For backwards compatibilty: If someone uses the old post($data) method.
308 * this method will be used to set the encoded data.
310 * $data can also be stream (such as file) from which the data will be read.
312 * @param string|resource $data
313 * @return Zend_Http_Client
315 public function setRawData($data)
317 $this->rawPostData = $data;
322 * Get the raw (already encoded) POST data.
326 public function getRawData()
328 return $this->rawPostData;
332 * Clear all GET and POST parameters
334 * Should be used to reset the request parameters if the client is
335 * used for several concurrent requests.
337 * clearAll parameter controls if we clean just parameters or also
340 * @param bool $clearAll Should all data be cleared?
341 * @return EasyRdf_Http_Client
343 public function resetParameters($clearAll = false)
345 // Reset parameter data
346 $this->paramsGet = array();
347 $this->rawPostData = null;
348 $this->method = 'GET';
351 $this->headers = array();
353 // Clear outdated headers
354 if (isset($this->headers['content-type'])) {
355 unset($this->headers['content-type']);
357 if (isset($this->headers['content-length'])) {
358 unset($this->headers['content-length']);
366 * Send the HTTP request and return an HTTP response object
368 * @return EasyRdf_Http_Response
369 * @throws EasyRdf_Exception
371 public function request($method = null)
374 throw new EasyRdf_Exception(
375 "Set URI before calling EasyRdf_Http_Client->request()"
380 $this->setMethod($method);
382 $this->redirectCounter = 0;
385 // Send the first request. If redirected, continue.
387 // Clone the URI and add the additional GET parameters to it
388 $uri = parse_url($this->uri);
389 if ($uri['scheme'] === 'http') {
390 $host = $uri['host'];
391 } elseif ($uri['scheme'] === 'https') {
392 $host = 'ssl://'.$uri['host'];
394 throw new EasyRdf_Exception(
395 "Unsupported URI scheme: ".$uri['scheme']
399 if (isset($uri['port'])) {
400 $port = $uri['port'];
402 if ($uri['scheme'] === 'https') {
409 if (!empty($this->paramsGet)) {
410 if (!empty($uri['query'])) {
411 $uri['query'] .= '&';
415 $uri['query'] .= http_build_query($this->paramsGet, null, '&');
418 $headers = $this->prepareHeaders($uri['host'], $port);
420 // Open socket to remote server
421 $socket = @fsockopen($host, $port, $errno, $errstr, $this->config['timeout']);
423 throw new EasyRdf_Exception("Unable to connect to $host:$port ($errstr)");
425 stream_set_timeout($socket, $this->config['timeout']);
426 $info = stream_get_meta_data($socket);
429 $path = $uri['path'];
433 if (isset($uri['query'])) {
434 $path .= '?' . $uri['query'];
436 fwrite($socket, "{$this->method} {$path} HTTP/1.1\r\n");
437 foreach ($headers as $k => $v) {
439 $v = ucfirst($k) . ": $v";
441 fwrite($socket, "$v\r\n");
443 fwrite($socket, "\r\n");
445 // Send the request body, if there is one set
446 if (isset($this->rawPostData)) {
447 fwrite($socket, $this->rawPostData);
450 // Read in the response
452 while (!feof($socket) && !$info['timed_out']) {
453 $content .= fgets($socket);
454 $info = stream_get_meta_data($socket);
457 if ($info['timed_out']) {
458 throw new EasyRdf_Exception("Request to $host:$port timed out");
461 // FIXME: support HTTP/1.1 100 Continue
466 // Parse the response string
467 $response = EasyRdf_Http_Response::fromString($content);
469 // If we got redirected, look for the Location header
470 if ($response->isRedirect() &&
471 ($location = $response->getHeader('location'))
474 // Avoid problems with buggy servers that add whitespace at the
475 // end of some headers (See ZF-11283)
476 $location = trim($location);
478 // Some servers return relative URLs in the location header
479 // resolve it in relation to previous request
480 $baseUri = new EasyRdf_ParsedUri($this->uri);
481 $location = $baseUri->resolve($location)->toString();
483 // If it is a 303 then drop the parameters and send a GET request
484 if ($response->getStatus() == 303) {
485 $this->resetParameters();
486 $this->setMethod('GET');
489 // If we got a well formed absolute URI
490 if (parse_url($location)) {
491 $this->setHeaders('host', null);
492 $this->setUri($location);
494 throw new EasyRdf_Exception(
495 "Failed to parse Location header returned by ".
499 ++$this->redirectCounter;
502 // If we didn't get any location, stop redirecting
507 } while ($this->redirectCounter < $this->config['maxredirects']);
513 * Prepare the request headers
518 protected function prepareHeaders($host, $port)
522 // Set the host header
523 if (! isset($this->headers['host'])) {
524 // If the port is not default, add it
525 if ($port !== 80 and $port !== 443) {
526 $host .= ':' . $port;
528 $headers[] = "Host: {$host}";
531 // Set the connection header
532 if (! isset($this->headers['connection'])) {
533 $headers[] = "Connection: close";
536 // Set the user agent header
537 if (! isset($this->headers['user-agent'])) {
538 $headers[] = "User-Agent: {$this->config['useragent']}";
541 // If we have rawPostData set, set the content-length header
542 if (isset($this->rawPostData)) {
543 $headers[] = "Content-Length: ".strlen($this->rawPostData);
546 // Add all other user defined headers
547 foreach ($this->headers as $header) {
548 list($name, $value) = $header;
549 if (is_array($value)) {
550 $value = implode(', ', $value);
553 $headers[] = "$name: $value";