FlameCore/Webtools

View on GitHub
lib/HttpClient.php

Summary

Maintainability
B
5 hrs
Test Coverage
<?php
/**
 * FlameCore Webtools
 * Copyright (C) 2015 IceFlame.net
 *
 * Permission to use, copy, modify, and/or distribute this software for
 * any purpose with or without fee is hereby granted, provided that the
 * above copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
 * FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
 * DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * @package  FlameCore\Webtools
 * @version  2.0
 * @link     http://www.flamecore.org
 * @license  http://opensource.org/licenses/ISC ISC License
 */

namespace FlameCore\Webtools;

/**
 * The HttpClient class
 *
 * @author   Christian Neff <christian.neff@gmail.com>
 */
class HttpClient
{
    const ENCODING_ALL = '';
    const ENCODING_GZIP = 'gzip';
    const ENCODING_DEFLATE = 'deflate';
    const ENCODING_IDENTITY = 'identity';

    const AUTH_BASIC = CURLAUTH_BASIC;
    const AUTH_NTLM = CURLAUTH_NTLM;

    const PROXY_HTTP = CURLPROXY_HTTP;
    const PROXY_SOCKS5 = CURLPROXY_SOCKS5;

    /**
     * The headers
     *
     * @var array
     */
    protected $headers = array();

    /**
     * The user agent string
     *
     * @var string
     */
    protected $useragent = 'Mozilla/5.0 (compatible; FlameCore Webtools/2.0)';

    /**
     * The timeout in seconds
     *
     * @var int
     */
    protected $timeout = 30;

    /**
     * The accepted encoding
     *
     * @var string
     */
    protected $encoding = self::ENCODING_ALL;

    /**
     * The curl handle
     *
     * @var resource
     */
    protected $handle;

    /**
     * Creates a HttpClient object.
     *
     * @param string $useragent The user agent string
     */
    public function __construct($useragent = null)
    {
        $this->handle = curl_init();

        curl_setopt($this->handle, CURLOPT_HEADER, false);
        curl_setopt($this->handle, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->handle, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, [$this, 'buffer']);

        $this->headers = array(
            'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Connection' => 'Keep-Alive'
        );

        if ($useragent !== null) {
            $this->setUserAgent($useragent);
        }
    }

    /**
     * Destructs the object.
     *
     * @return void
     */
    public function __destruct()
    {
        curl_close($this->handle);
    }

    /**
     * Executes a GET request.
     *
     * @param string $url The URL to make the request to
     * @param array $headers Optional extra headers
     * @return object Returns an object containing the response information.
     */
    public function get($url, array $headers = array())
    {
        curl_setopt($this->handle, CURLOPT_HTTPGET, true);

        return $this->execute($url, $headers);
    }

    /**
     * Executes a POST request.
     *
     * @param string $url The URL to make the request to
     * @param array|string $data The full data to post in the operation
     * @param array $headers Optional extra headers
     * @return object Returns an object containing the response information.
     */
    public function post($url, $data, array $headers = array())
    {
        curl_setopt($this->handle, CURLOPT_POST, true);
        curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);

        return $this->execute($url, $headers);
    }

    /**
     * Executes a PUT request.
     *
     * @param string $url The URL to make the request to
     * @param array|string $data The full data to post in the operation
     * @param array $headers Optional extra headers
     * @return object Returns an object containing the response information.
     */
    public function put($url, $data, array $headers = array())
    {
        curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, 'PUT');

        $fields = is_array($data) ? http_build_query($data) : $data;
        curl_setopt($this->handle, CURLOPT_POSTFIELDS, $fields);

        $headers = array_replace($headers, ['Content-Length' => strlen($fields)]);
        return $this->execute($url, $headers);
    }

    /**
     * Executes a PUT request using a file.
     *
     * @param string $url The URL to make the request to
     * @param string|resource $file The file that the transfer should be read from when uploading
     * @param array $headers Optional extra headers
     * @return object Returns an object containing the response information.
     */
    public function putFile($url, $file, array $headers = array())
    {
        $file = $this->openFile($file);
        $stat = fstat($file);

        curl_setopt($this->handle, CURLOPT_PUT, true);
        curl_setopt($this->handle, CURLOPT_INFILE, $file);
        curl_setopt($this->handle, CURLOPT_INFILESIZE, $stat['size']);

        return $this->execute($url, $headers);
    }

    /**
     * Executes a custom request.
     *
     * @param string $method The custom request method verb
     * @param string $url The URL to make the request to
     * @param array|string $data The data to post in the operation
     * @param array $headers Optional extra headers
     * @return object Returns an object containing the response information.
     */
    public function request($method, $url, $data = null, array $headers = array())
    {
        curl_setopt($this->handle, CURLOPT_CUSTOMREQUEST, strtoupper($method));

        if (!empty($data)) {
            curl_setopt($this->handle, CURLOPT_POSTFIELDS, $data);
        }

        return $this->execute($url, $headers);
    }

    /**
     * Gets all defined headers.
     *
     * @return array Returns the headers.
     */
    public function getHeaders()
    {
        return $this->headers;
    }

    /**
     * Sets a header.
     *
     * @param string $name The name of the header
     * @param string $value The value of the header
     */
    public function setHeader($name, $value)
    {
        $this->headers[$name] = $value;
    }

    /**
     * Sets multiple headers.
     *
     * @param array $headers The headers to set
     */
    public function setHeaders(array $headers)
    {
        $this->headers = array_merge($this->headers, $headers);
    }

    /**
     * Gets the user agent string.
     *
     * @return string
     */
    public function getUserAgent()
    {
        return $this->useragent;
    }

    /**
     * Sets the user agent string.
     *
     * @param string $useragent The new user agent string
     */
    public function setUserAgent($useragent)
    {
        $this->useragent = (string) $useragent;
    }

    /**
     * Gets the timeout.
     *
     * @return int Returns the timeout in seconds.
     */
    public function getTimeout()
    {
        return $this->timeout;
    }

    /**
     * Sets the timeout.
     *
     * @param int $timeout The new timeout in seconds
     */
    public function setTimeout($timeout)
    {
        $this->timeout = (int) $timeout;
    }

    /**
     * Gets the encoding.
     *
     * @return string Returns the encoding.
     */
    public function getEncoding()
    {
        return $this->encoding;
    }

    /**
     * Sets the encoding.
     *
     * @param string $encoding The encoding to use. This can be any of the `HttpClient::ENCODING_*` constants.
     */
    public function setEncoding($encoding)
    {
        if (!in_array($encoding, [self::ENCODING_IDENTITY, self::ENCODING_DEFLATE, self::ENCODING_GZIP, self::ENCODING_ALL])) {
            throw new \InvalidArgumentException('The encoding must be one of: HttpClient::ENCODING_IDENTITY, HttpClient::ENCODING_DEFLATE, HttpClient::ENCODING_GZIP, HttpClient::ENCODING_ALL.');
        }

        $this->encoding = $encoding;
    }

    /**
     * Enables the use of cookies.
     *
     * @param string $jarfile The full path to the file where cookies are saved
     * @throws \InvalidArgumentException if the given parameter is invalid.
     * @throws \LogicException if the cookie file could not be opened.
     */
    public function acceptCookies($jarfile = null)
    {
        $jarfile = $jarfile ? (string) $jarfile : sys_get_temp_dir().DIRECTORY_SEPARATOR.'cookies.txt';

        if (!is_file($jarfile) && !touch($jarfile)) {
            throw new \LogicException(sprintf('Cookie file "%s" could not be opened. Make sure that the directory is writable.', $jarfile));
        }

        curl_setopt($this->handle, CURLOPT_COOKIEFILE, $jarfile);
        curl_setopt($this->handle, CURLOPT_COOKIEJAR, $jarfile);
    }

    /**
     * Enables the use of a proxy.
     *
     * @param string $proxy The proxy to use. Use `@` to separate credentials and address.
     * @param int $type The type of proxy. This can be one of: `HttpClient::PROXY_HTTP` (default), `HttpClient::PROXY_SOCKS5`.
     * @param int $auth The HTTP authentication method(s) to use for the proxy connection.
     *   This can be one of: `HttpClient::AUTH_BASIC` (default), `HttpClient::AUTH_NTLM`.
     * @throws \InvalidArgumentException if the given parameter is invalid.
     */
    public function useProxy($proxy, $type = self::PROXY_HTTP, $auth = self::AUTH_BASIC)
    {
        $proxy = (string) $proxy;

        if (!in_array($type, [self::PROXY_HTTP, self::PROXY_SOCKS5])) {
            throw new \InvalidArgumentException('The $type parameter must be one of: HttpClient::PROXY_HTTP, HttpClient::PROXY_SOCKS5.');
        }

        if (!in_array($auth, [self::AUTH_BASIC, self::AUTH_NTLM])) {
            throw new \InvalidArgumentException('The $auth parameter must be one of: HttpClient::AUTH_BASIC, HttpClient::AUTH_NTLM.');
        }

        if (strpos($proxy, '@') !== false) {
            list($proxyCredentials, $proxyAddress) = explode('@', $proxy, 2);
            curl_setopt($this->handle, CURLOPT_PROXY, $proxyAddress);
            curl_setopt($this->handle, CURLOPT_PROXYUSERPWD, $proxyCredentials);
        } else {
            curl_setopt($this->handle, CURLOPT_PROXY, $proxy);
        }
    }

    /**
     * Really executes a request to the given URL.
     *
     * @param string $url The URL to fetch
     * @param array $headers Optional extra headers
     * @return object Returns an object containing the response information.
     */
    protected function execute($url, array $headers = array())
    {
        curl_setopt($this->handle, CURLOPT_URL, $url);

        curl_setopt($this->handle, CURLOPT_USERAGENT, $this->useragent);
        curl_setopt($this->handle, CURLOPT_TIMEOUT, $this->timeout);
        curl_setopt($this->handle, CURLOPT_ENCODING, $this->encoding);

        $curlheaders = array();

        $headers = array_merge($this->headers, $headers);
        foreach ($headers as $headerName => $headerValue) {
            $curlheaders[] = "$headerName: $headerValue";
        }

        curl_setopt($this->handle, CURLOPT_HTTPHEADER, $curlheaders);

        $response = curl_exec($this->handle);
        if ($response !== false) {
            $info = curl_getinfo($this->handle);
            if ($info && $info['http_code'] >= 200 && $info['http_code'] < 300) {
                $headers = $this->buffer();

                $info['success'] = true;
                $info['headers'] = $this->parseHeaders($headers);
                $info['data'] = $response;
            } else {
                $info['success'] = false;
            }
        } else {
            $info = array();
            $info['success'] = false;
            $info['error'] = curl_errno($this->handle);
            $info['error_text'] = curl_error($this->handle);
        }

        return (object) $info;
    }

    /**
     * Parses the given HTTP headers.
     *
     * @param string $rawHeaders The raw HTTP headers
     * @return array
     */
    protected function parseHeaders($rawHeaders)
    {
        $headers = array();

        $lines = explode("\r\n", preg_replace('/\x0D\x0A[\x09\x20]+/', ' ', $rawHeaders));
        foreach ($lines as $header) {
            if (preg_match('/([^:]+): (.+)/m', $header, $match)) {
                if (!isset($headers[$match[1]])) {
                    $headers[$match[1]] = trim($match[2]);
                } elseif (is_array($headers[$match[1]])) {
                    $headers[$match[1]][] = trim($match[2]);
                } else {
                    $headers[$match[1]] = array($headers[$match[1]], trim($match[2]));
                }
            }
        }

        return $headers;
    }

    /**
     * Opens a file handle.
     *
     * @param string|resource $file The file to open
     * @return resource
     */
    private function openFile($file)
    {
        if (is_resource($file)) {
            if (get_resource_type($file) !== 'stream') {
                throw new \InvalidArgumentException('The given resource is not a file handle.');
            }
        } else {
            $filename = (string) $file;

            if (!is_file($filename) || !is_readable($filename)) {
                throw new \LogicException(sprintf('File "%s" could not be opened.', $filename));
            }

            $file = fopen($filename, 'r');
        }

        return $file;
    }

    /**
     * Buffers the line read by curl. When no handle is given, returns and clears the content of the buffer.
     *
     * @param resource $curl The curl handle
     * @param string $line The current line
     * @return int|string
     */
    private function buffer($curl = null, $line = null)
    {
        static $buffer;

        if ($curl) {
            $buffer .= $line;

            return strlen($line);
        } else {
            $return = $buffer;
            $buffer = '';

            return $return;
        }
    }
}