php-yaoi/php-yaoi

View on GitHub
src/Http/Client.php

Summary

Maintainability
D
2 days
Test Coverage
<?php
namespace Yaoi\Http;

use Yaoi\Http\Client\Driver;
use Yaoi\Http\Client\Settings;
use Yaoi\Http\Client\UploadFile;
use Yaoi\Log;
use Yaoi\Mock;
use Yaoi\Mock\Able;

/**
 * Class Http_Client
 * @method Driver getDriver()
 */
class Client extends \Yaoi\Service implements Able
{
    const XML_HTTP_REQUEST = 'XMLHttpRequest';

    public $cookies = array();
    public $requestCharset = 'UTF-8';
    public $charset = 'UTF-8';
    /** @var  null|array */
    public $post;
    public $url; // TODO no direct access to url
    public $referrer;
    public $xRequestedWith;
    public $followLocation = true;
    public $skipBadRequestException = true;
    public $responseHeaders = array();
    public $parsedHeaders = array();
    public $headers = array();
    public $defaultHeaders = array(
        'User-Agent' => 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0',
        'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
        'Accept-Encoding' => 'gzip, deflate',
        'Accept-Language' => 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
        'Connection' => 'close',
    );

    /**
     * @var Settings
     */
    protected $settings;

    public function __construct($settings = null)
    {
        parent::__construct($settings);
        $this->reset();
        if ($this->settings) {
            if ($this->settings->proxy) {
                $this->setProxy($this->settings->proxy);
            }

            if ($this->settings->defaultHeaders) {
                $this->defaultHeaders = array_merge($this->defaultHeaders, $this->settings->defaultHeaders);
            }

            if ($this->settings->log) {
                $log = Log::getInstance($this->settings->log);
                $this->logUrl($log);
                $this->logContext($log);
                $this->logResponseHeaders($log);
                $this->logResponseBody($log);
                $this->logError($log);
            }
        }

        $this->mock = Mock::getNull();
    }

    public function reset()
    {
        $this->headers = $this->defaultHeaders;
        $this->post = null;
        $this->url = null;

        return $this;
    }

    protected function parseResponseCookies()
    {
        $cookies = array();
        $this->parsedHeaders = array();
        foreach ($this->responseHeaders as $hdr) {
            if ($p = strpos($hdr, ':')) {
                $header = substr($hdr, 0, $p);
                $value = trim(substr($hdr, $p + 1));

                $tmp = explode(';', $value);
                $valueParams = array(
                    'value' => $value,
                );
                if (count($tmp) > 1) {
                    foreach ($tmp as $tm) {
                        $tm = explode('=', trim($tm), 2);
                        if (isset($tm[1])) {
                            $valueParams[$tm[0]] = $tm[1];
                        } else {
                            $valueParams['baseValue'] = $tm[0];
                        }
                    }
                }

                $this->parsedHeaders [strtolower($header)] = $valueParams;
            }

            if (preg_match('/^Set-Cookie:\s*([^;]+)/', $hdr, $matches)) {
                //echo $hdr;
                parse_str($matches[1], $tmp);
                $cookies = array_merge($cookies, $tmp);
            }

            //Content-Type: text/html; charset=WINDOWS-1251
        }

        if (isset($this->parsedHeaders['content-type']['charset'])) {
            $this->charset = $this->parsedHeaders['content-type']['charset'];
        }

        $this->cookies = array_merge($this->cookies, $cookies);
    }


    protected $proxy;

    public function setProxy($dsn)
    {
        if ($dsn instanceof Settings) {
            $this->proxy = $dsn;
        } else {
            $this->proxy = new Settings($dsn);
        }
    }


    private $redirectsCount = 0;

    public function fetch($url = null)
    {
        if (null !== $url) {
            if (strpos($url, '://') === false) {
                $url = $this->getAbsoluteUrl($url);
            }
            $this->url = $url;
        }

        $driver = $this->getDriver();
        $driver->reset();

        if ($this->proxy) {
            $driver->setProxy($this->proxy);
        }

        $headers = $this->headers;
        if (!empty($this->cookies)) {
            $headers['Cookie'] = http_build_query($this->cookies, null, '; ');
        }


        $uploadingFiles = false;
        if ($this->post) {
            foreach ($this->post as $item) {
                if ($item instanceof UploadFile) {
                    $uploadingFiles = true;
                    break;
                }
            }
        }


        if ($uploadingFiles) {
            $headers = $this->prepareUpload($driver, $headers);
        } elseif ($this->post) {
            $driver->setMethod('POST');
            array_walk_recursive($this->post, function (&$item){
                if (is_object($item)) {
                    $item = (string)$item;
                }
            });

            $content = http_build_query($this->post);
            $driver->setRequestContent($content);
            $headers['Content-Type'] = 'application/x-www-form-urlencoded';
            $headers['Content-Length'] = strlen($content);
            unset($content);
        }

        if ($this->referrer) {
            $headers['Referer'] = $this->referrer;
        }
        if ($this->xRequestedWith) {
            $headers['X-Requested-With'] = $this->xRequestedWith;
            $this->xRequestedWith = null;
        }
        $this->referrer = $this->url;

        if (isset($headers['Content-Type']) && !$uploadingFiles) {
            $headers['Content-Type'] .= '; charset=' . $this->requestCharset;
        }

        $driver->setHeaders($headers);
        $driver->setUrl($this->url);

        if ($this->logUrl) {
            $this->logUrl->push($this->url);
        }

        if ($this->logPost) {
            $this->logPost->push(print_r($this->post, 1));
        }

        $this->responseHeaders = array();

        $mock = $this->mock->branch($this->url, hash('crc32b', serialize($driver->getRequest())));
        if ($this->logContext) {
            $this->logContext->push($driver->getRequest());
        }

        $self = $this;
        $response = $this->performFetch($mock, $driver, $self);


        if ($this->logResponseHeaders) {
            $this->logResponseHeaders->push(print_r($this->responseHeaders, 1));
        }

        if ($this->logRawResponseBody) {
            $this->logRawResponseBody->push($response);
        }


        $this->parseResponseCookies();

        if ($this->followLocation) {
            if (!empty($this->parsedHeaders['location'])) {
                $this->post = null;
                $redirectUrl = $this->parsedHeaders['location']['value'];
                $this->url = $this->getAbsoluteUrl($redirectUrl);
                if (++$this->redirectsCount > 5) {
                    return false;
                }
                return $this->fetch();
            }
        }


        $response = $this->decodeContent($response);

        if ($this->logResponseBody) {
            $this->logResponseBody->push($response);
        }

        return $response;
    }


    public function getAbsoluteUrl($url)
    {
        $parsed = parse_url($this->url);

        // TODO //host/path, /path, scheme://host/path, path, ?query

        // "//host/..."
        if ('//' == substr($url, 0, 2)) {
            $url = $parsed['scheme'] . ':' . $url;
        } // "/path..."
        elseif ('/' == $url[0]) {
            $base = substr($this->url, 0, strpos($this->url, '/', 8));
            $url = $base . $url;
        } // "http(s)://...."
        elseif (strpos($url, '://')) {

        } // "?query..."
        elseif ('?' == $url[0]) {
            $pos = strpos($this->url, '?');
            $base = $pos ? substr($this->url, 0, $pos) : $this->url;
            $url = $base . $url;
        } // "path..."
        else {
            $pos = strrpos($this->url, '/');
            $base = $pos ? substr($this->url, 0, $pos + 1) : $this->url;
            $url = $base . $url;
        }

        return $url;
    }


    /**
     * @var Mock
     */
    private $mock;

    public function mock(Mock $dataSet = null)
    {
        if (null === $dataSet) {
            $dataSet = Mock::getNull();
        }
        $this->mock = $dataSet;
    }


    /**
     * @var Log
     */
    private $logError;

    public function logError(Log $log = null)
    {
        $this->logError = $log;
    }


    /**
     * @var Log
     */
    private $logUrl;

    public function logUrl(Log $log = null)
    {
        $this->logUrl = $log;
        return $this;
    }


    /**
     * @var Log
     */
    private $logPost;

    public function logPost(Log $log = null)
    {
        $this->logPost = $log;
        return $this;
    }

    /**
     * @var Log
     */
    private $logContext;

    public function logContext(Log $log = null)
    {
        $this->logContext = $log;
        return $this;
    }

    /**
     * @var Log
     */
    private $logResponseHeaders;

    public function logResponseHeaders(Log $log = null)
    {
        $this->logResponseHeaders = $log;
        return $this;
    }


    /**
     * @var Log
     */
    private $logRawResponseBody;

    public function logRawResponseBody(Log $log = null)
    {
        $this->logRawResponseBody = $log;
        return $this;
    }

    /**
     * @var Log
     */
    private $logResponseBody;

    public function logResponseBody(Log $log = null)
    {
        $this->logResponseBody = $log;
        return $this;
    }

    protected static function getSettingsClassName()
    {
        return Settings::className();
    }

    /**
     * @param $driver
     * @param $headers
     * @return mixed
     * @throws \Yaoi\Service\Exception
     */
    private function prepareUpload($driver, $headers)
    {
        $driver->setMethod('POST');

        $multipartBoundary = '--------------------------' . \Yaoi\Date\TimeMachine::getInstance()->microNow();
        $content = '';

        foreach ($this->post as $name => $value) {
            if ($value instanceof UploadFile) {
                $content .= "--" . $multipartBoundary . "\r\n"
                    . "Content-Disposition: form-data; name=\"" . $name . "\"; filename=\"" . $value->getFileName() . "\"\r\n"
                    . "Content-Type: " . $value->mimeType . "\r\n\r\n"
                    . $value->getContents() . "\r\n";
            } else {
                $content .= "--" . $multipartBoundary . "\r\n"
                    . "Content-Disposition: form-data; name=\"$name\"\r\n\r\n"
                    . "$value\r\n";
            }
        }
        $content .= "--" . $multipartBoundary . "--\r\n";

        $driver->setRequestContent($content);
        $headers['Content-Type'] = 'multipart/form-data; boundary=' . $multipartBoundary;
        unset($content);
        return $headers;
    }

    /**
     * @param $response
     * @return string
     */
    private function decodeContent($response)
    {
        if (!empty($this->parsedHeaders['content-encoding'])) {
            if ($response && 'gzip' == strtolower($this->parsedHeaders['content-encoding']['value'])) {
                if (!function_exists('gzdecode')) {
                    $response = gzinflate(substr($response, 10, -8));
                    return $response;
                } else {
                    $response = gzdecode($response);
                    return $response;
                }
            } elseif ('deflate' == strtolower($this->parsedHeaders['content-encoding']['value'])) {
                $response = gzinflate($response);
                return $response;
            }
            return $response;
        }
        return $response;
    }

    /**
     * @param $mock
     * @param $driver
     * @param $self
     * @return mixed
     * @throws Client\Exception
     * @throws \Exception
     */
    private function performFetch($mock, $driver, $self)
    {
        try {
            list($response, $this->responseHeaders) = $mock->branch('responseData')
                ->get(null, function () use ($driver, $self, $mock) {
                    $driver->fetch();
                    $response = $driver->getResponseContent();
                    $responseHeaders = $driver->getResponseHeaders();

                    if (!$self->skipBadRequestException && false === $response) {
                        $e = new Client\Exception('Bad request', Client\Exception::BAD_REQUEST);
                        $e->request = $driver->getRequest();
                        $e->responseHeaders = $self->responseHeaders;
                        $e->url = $self->url;
                        throw $e;
                    }

                    return array($response, $responseHeaders);
                });
            return $response;
        } catch (Client\Exception $e) {
            if ($this->logError) {
                $this->logError->push($e->getMessage()
                    . ', request: ' . print_r($driver->getRequest(), 1)
                    . ', serialize: ' . base64_encode(serialize($driver->getRequest())));
            }
            throw $e;
        }
        return $response;
    }


}