Source of file CurlWrapper.php

Size: 33,440 Bytes - Last Modified: 2020-10-25T23:00:04+00:00

/root/gitwork/work/tornelib-php-netcurl-6.1/src/Module/Network/Wrappers/CurlWrapper.php

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168
<?php
/**
 * Copyright © Tomas Tornevall / Tornevall Networks. All rights reserved.
 * See LICENSE.md for license details.
 */

/** @noinspection PhpComposerExtensionStubsInspection */

namespace TorneLIB\Module\Network\Wrappers;

use Exception;
use ReflectionException;
use TorneLIB\Config\Flag;
use TorneLIB\Exception\Constants;
use TorneLIB\Exception\ExceptionHandler;
use TorneLIB\Helpers\GenericParser;
use TorneLIB\Helpers\Version;
use TorneLIB\Model\Interfaces\WrapperInterface;
use TorneLIB\Model\Type\authType;
use TorneLIB\Model\Type\dataType;
use TorneLIB\Model\Type\requestMethod;
use TorneLIB\Module\Config\WrapperConfig;
use TorneLIB\Module\Config\WrapperCurlOpt;
use TorneLIB\Utils\Generic;
use TorneLIB\Utils\Security;

try {
    Version::getRequiredVersion();
} catch (Exception $e) {
    die($e->getMessage());
}

/**
 * Class CurlWrapper.
 *
 * Wrapper to make calls directly to the curl engine. This should not be used primarily if auto detection is the
 * preferred way to fetch data.
 *
 * @package TorneLIB\Module\Network\Wrappers
 */
class CurlWrapper implements WrapperInterface
{
    /**
     * @var WrapperConfig $CONFIG
     * @since 6.1.0
     */
    private $CONFIG;

    /**
     * @var resource cURL simple handle
     * @since 6.1.0
     */
    private $curlHandle;

    /**
     * @var
     * @since 6.1.0
     */
    private $curlResponse;

    /**
     * @var int
     * @since 6.1.0
     */
    private $curlHttpCode = 0;

    /**
     * @var array
     * @since 6.1.0
     */
    private $curlMultiHttpCode = [];

    /**
     * @var array
     * @since 6.1.0
     */
    private $curlResponseHeaders = [];

    /**
     * @var bool
     * @since 6.1.0
     */
    private $isCurlMulti = false;

    /**
     * @var resource cURL multi handle
     * @since 6.1.0
     */
    private $curlMultiHandle;

    /**
     * @var
     * @since 6.1.0
     */
    private $curlMultiErrors;

    /**
     * @var bool
     * @since 6.1.0
     */
    private $instantCurlMultiErrors = false;

    /**
     * @var array
     * @since 6.1.0
     */
    private $curlMultiHandleObjects = [];

    /**
     * @var
     * @since 6.1.0
     */
    private $curlMultiResponse;

    /**
     * @var array
     * @since 6.1.0
     */
    private $customPreHeaders = [];

    /**
     * Static headers that will not reset between each request-init.
     * @var array
     * @since 6.1.2
     */
    private $customPreHeadersStatic = [];

    /**
     * @var array
     * @since 6.1.0
     */
    private $customHeaders = [];

    /**
     * @var string Custom content type.
     * @since 6.1.0
     */
    private $contentType = '';

    /**
     * @var bool If resources are checked strictly.
     * @since 6.1.2
     */
    private $strictResource = false;

    /**
     * CurlWrapper constructor.
     *
     * @throws ExceptionHandler
     * @throws Exception
     * @since 6.1.0
     */
    public function __construct()
    {
        // Make sure there are available drivers before using the wrapper.
        Security::getCurrentFunctionState('curl_init');
        Security::getCurrentFunctionState('curl_exec');

        $this->CONFIG = new WrapperConfig();
        $this->CONFIG->setCurrentWrapper(__CLASS__);
        $hasConstructorArguments = $this->getPriorCompatibilityArguments(func_get_args());

        if ($hasConstructorArguments) {
            $this->initCurlHandle();
        }
    }

    /**
     * Reverse compatibility with v6.0 - returns true if any of the settings here are touched.
     *
     * @param array $funcArgs
     * @return bool
     * @throws Exception
     * @since 6.1.0
     */
    private function getPriorCompatibilityArguments($funcArgs = [])
    {
        return $this->CONFIG->getCompatibilityArguments($funcArgs);
    }

    /**
     * Initialize simple or multi curl handles.
     *
     * @return $this
     * @throws ExceptionHandler
     * @since 6.1.0
     */
    private function initCurlHandle()
    {
        $this->isCurlMulti = false;

        if (is_string($this->CONFIG->getRequestUrl())) {
            $requestUrl = $this->CONFIG->getRequestUrl();
            if (!empty($requestUrl) &&
                filter_var($this->CONFIG->getRequestUrl(), FILTER_VALIDATE_URL)
            ) {
                $this->curlHandle = curl_init();
                $this->setupHandle($this->curlHandle, $this->CONFIG->getRequestUrl());
            } else {
                $this->curlHandle = curl_init();
                $this->setupHandle($this->curlHandle, null);
            }
        } else {
            // Prepare for multiple curl requests.
            $requestUrlArray = $this->CONFIG->getRequestUrl();
            if (is_array($requestUrlArray) && count($requestUrlArray)) {
                $this->isCurlMulti = true;
                $this->curlMultiHandle = curl_multi_init();
                foreach ($requestUrlArray as $url) {
                    $this->curlMultiHandleObjects[$url] = curl_init();
                    $this->setupHandle(
                        $this->curlMultiHandleObjects[$url],
                        $url
                    );
                }
                $this->setCurlMultiHandles();
            }
        }

        return $this;
    }

    /**
     * Major initializer.
     *
     * @param $curlHandle
     * @param $url
     * @return $this
     * @throws ExceptionHandler
     * @since 6.1.0
     */
    private function setupHandle($curlHandle, $url)
    {
        $this->setCurlAuthentication($curlHandle);
        $this->setCurlDynamicValues($curlHandle);
        $this->setCurlSslValues($curlHandle);
        $this->setCurlStaticValues($curlHandle);
        $this->setCurlPostData($curlHandle);
        $this->setCurlRequestMethod($curlHandle);
        $this->setCurlCustomHeaders($curlHandle);
        if (!empty($url)) {
            $this->setOptionCurl($curlHandle, CURLOPT_URL, $url);
        }

        return $this;
    }

    /**
     * @param $curlHandle
     * @return CurlWrapper
     * @since 6.1.0
     */
    private function setCurlAuthentication($curlHandle)
    {
        $authData = $this->getAuthentication();
        if (!empty($authData['password'])) {
            $this->setOptionCurl($curlHandle, CURLOPT_HTTPAUTH, $authData['type']);
            $this->setOptionCurl(
                $curlHandle,
                CURLOPT_USERPWD,
                sprintf('%s:%s', $authData['username'], $authData['password'])
            );
        }

        return $this;
    }

    /**
     * @return array
     * @since 6.1.0
     */
    public function getAuthentication()
    {
        return $this->CONFIG->getAuthentication();
    }

    /**
     * Set curloptions.
     *
     * @param $curlHandle
     * @param $curlOpt
     * @param $value
     * @return bool
     * @since 6.1.0
     */
    public function setOptionCurl($curlHandle, $curlOpt, $value)
    {
        $this->CONFIG->setOption($curlOpt, $value);
        return curl_setopt($curlHandle, $curlOpt, $value);
    }

    /**
     * @param $curlHandle
     * @return CurlWrapper
     * @throws ExceptionHandler
     * @since 6.1.0
     */
    private function setCurlDynamicValues($curlHandle)
    {
        foreach ($this->CONFIG->getOptions() as $curlKey => $curlValue) {
            $this->setOptionCurl($curlHandle, $curlKey, $curlValue);
        }

        return $this;
    }

    /**
     * @param $curlHandle
     * @return CurlWrapper
     * @since 6.1.0
     */
    private function setCurlSslValues($curlHandle)
    {
        // version_compare(PHP_VERSION, '5.4.11', ">=")
        if (PHP_VERSION_ID >= 50411) {
            $this->setOptionCurl($curlHandle, WrapperCurlOpt::NETCURL_CURLOPT_SSL_VERIFYHOST, 2);
        } else {
            $this->setOptionCurl($curlHandle, WrapperCurlOpt::NETCURL_CURLOPT_SSL_VERIFYHOST, 1);
        }
        // CURLOPT_SSL_VERIFYPEER is available starting with PHP 7.1
        $this->setOptionCurl($curlHandle, WrapperCurlOpt::NETCURL_CURLOPT_SSL_VERIFYPEER, 1);

        return $this;
    }

    /**
     * Values set here can not be changed via any other part of the wrapper.
     *
     * @param $curlHandle
     * @return $this
     * @since 6.1.0
     */
    private function setCurlStaticValues($curlHandle)
    {
        $this->setOptionCurl($curlHandle, CURLOPT_RETURNTRANSFER, true);
        $this->setOptionCurl($curlHandle, CURLOPT_HEADER, false);
        $this->setOptionCurl($curlHandle, CURLOPT_AUTOREFERER, true);
        $this->setOptionCurl($curlHandle, CURLINFO_HEADER_OUT, true);
        $this->setOptionCurl($curlHandle, CURLOPT_HEADERFUNCTION, [$this, 'getCurlHeaderRow']);

        return $this;
    }

    /**
     * @param $curlHandle
     * @return CurlWrapper
     * @throws ExceptionHandler
     * @since 6.1.0
     */
    private function setCurlPostData($curlHandle)
    {
        $requestData = $this->CONFIG->getRequestData();

        switch ($this->CONFIG->getRequestDataType()) {
            case dataType::XML:
                $this->setCurlPostXmlHeader($curlHandle, $requestData);
                break;
            case dataType::JSON:
                $this->setCurlPostJsonHeader($curlHandle, $requestData);
                break;
            default:
                if ($this->CONFIG->getRequestMethod() === requestMethod::METHOD_POST) {
                    $this->setOptionCurl($curlHandle, CURLOPT_POST, true);
                }
                $this->setOptionCurl($curlHandle, CURLOPT_POSTFIELDS, $requestData);
                break;
        }

        return $this;
    }

    /**
     * @param $curlHandle
     * @param $requestData
     * @return $this
     * @throws ExceptionHandler
     * @since 6.1.0
     * @todo Convert arrayed data to XML.
     */
    public function setCurlPostXmlHeader($curlHandle, $requestData)
    {
        if (is_array($requestData)) {
            throw new ExceptionHandler(
                'Convert arrayed data to XML error - no data present!',
                Constants::LIB_UNHANDLED
            );
        }

        $this->customPreHeaders['Content-Type'] = 'Content-Type: text/xml; charset=utf-8';
        $this->customPreHeaders['Content-Length'] = strlen($requestData);
        $this->setOptionCurl($curlHandle, CURLOPT_POSTFIELDS, $requestData);

        return $this;
    }

    /**
     * @param $curlHandle
     * @param $requestData
     * @return $this
     * @since 6.1.0
     */
    private function setCurlPostJsonHeader($curlHandle, $requestData)
    {
        $jsonContentType = 'application/json; charset=utf-8';

        $testContentType = $this->getContentType();
        if (false !== stripos($testContentType, "json")) {
            $jsonContentType = $testContentType;
        }

        $this->customPreHeaders['Content-Type'] = $jsonContentType;
        $this->customPreHeaders['Content-Length'] = strlen($requestData);
        $this->setOptionCurl($curlHandle, CURLOPT_POSTFIELDS, $requestData);

        return $this;
    }

    /**
     * @return string
     * @since 6.0.17
     */
    public function getContentType()
    {
        return $this->contentType;
    }

    /**
     * @param string $setContentTypeString
     * @return CurlWrapper
     * @since 6.0.17
     */
    public function setContentType($setContentTypeString = 'application/json; charset=utf-8')
    {
        $this->contentType = $setContentTypeString;

        return $this;
    }

    /**
     * @param $curlHandle
     * @return CurlWrapper
     * @since 6.1.0
     */
    private function setCurlRequestMethod($curlHandle)
    {
        switch ($this->CONFIG->getRequestMethod()) {
            case requestMethod::METHOD_POST:
                $this->setOptionCurl($curlHandle, CURLOPT_CUSTOMREQUEST, 'POST');
                break;
            case requestMethod::METHOD_DELETE:
                $this->setOptionCurl($curlHandle, CURLOPT_CUSTOMREQUEST, 'DELETE');
                break;
            case requestMethod::METHOD_HEAD:
                $this->setOptionCurl($curlHandle, CURLOPT_CUSTOMREQUEST, 'HEAD');
                break;
            case requestMethod::METHOD_PUT:
                $this->setOptionCurl($curlHandle, CURLOPT_CUSTOMREQUEST, 'PUT');
                break;
            case requestMethod::METHOD_REQUEST:
                $this->setOptionCurl($curlHandle, CURLOPT_CUSTOMREQUEST, 'REQUEST');
                break;
            default:
                // Making sure we send data in proper formatting if there is bad user configuration.
                // Bad configuration is when both GET+POST data parameters are sent as a GET when the
                // correct set up in that case is a POST.
                $this->setOptionCurl($curlHandle, CURLOPT_CUSTOMREQUEST, 'GET');
                break;
        }

        return $this;
    }

    /**
     * @param $curlHandle
     * @return CurlWrapper
     * @since 6.1.0
     */
    private function setCurlCustomHeaders($curlHandle)
    {
        $this->setProperCustomHeader();
        $this->setupHeaders($curlHandle);
        return $this;
    }

    /**
     * Fix problematic header data by converting them to proper outputs.
     *
     * @return $this
     * @since 6.1.0
     */
    private function setProperCustomHeader()
    {
        // Merge static header data into customPreHeaders.
        foreach ($this->customPreHeadersStatic as $headerKey => $headerValue) {
            $this->customPreHeaders[$headerKey] = $headerValue;
        }

        foreach ($this->customPreHeaders as $headerKey => $headerValue) {
            $testHead = explode(":", $headerValue, 2);
            if (isset($testHead[1])) {
                $this->customHeaders[] = $headerValue;
            } elseif (!is_numeric($headerKey)) {
                $this->customHeaders[] = $headerKey . ": " . $headerValue;
            }
            unset($this->customPreHeaders[$headerKey]);
        }

        return $this;
    }

    /**
     * @param $curlHandle
     * @return $this
     * @since 6.1.0
     */
    private function setupHeaders($curlHandle)
    {
        if (count($this->customHeaders)) {
            $this->setOptionCurl($curlHandle, CURLOPT_HTTPHEADER, $this->customHeaders);
        }

        return $this;
    }

    /**
     * @return $this
     * @since 6.1.0
     */
    private function setCurlMultiHandles()
    {
        $reqUrlArray = (array)$this->CONFIG->getRequestUrl();
        foreach ($reqUrlArray as $url) {
            curl_multi_add_handle($this->curlMultiHandle, $this->curlMultiHandleObjects[$url]);
        }

        return $this;
    }

    /**
     * @return string
     * @throws ExceptionHandler
     * @throws ReflectionException
     * @since 6.1.0
     */
    public function getVersion()
    {
        return isset($this->version) && !empty($this->version) ?
            $this->version : (new Generic())->getVersionByAny(__DIR__, 3, WrapperConfig::class);
    }

    /**
     * Destructor for cleaning up resources.
     * @since 6.1.0
     */
    public function __destruct()
    {
        if ($this->isCurlResource($this->curlHandle)) {
            curl_close($this->curlHandle);
        }
        if ($this->isCurlMulti) {
            curl_multi_close($this->curlMultiHandle);
        }
        $this->resetCurlRequest();
    }

    /**
     * Reset curl on each new curlrequest to make sure old responses is no longer present.
     * @since 6.1.0
     */
    private function resetCurlRequest()
    {
        /** @noinspection PhpUndefinedMethodInspection */
        $this->strictResource = Flag::isFlag('strict_resource');
        $this->customHeaders = [];
        $this->curlResponseHeaders = [];
        $this->curlMultiErrors = null;
        $this->curlMultiResponse = null;
        $this->curlMultiHandle = null;
        $this->curlMultiHandleObjects = [];
        return $this;
    }

    /**
     * @param mixed $key
     * @param string $value
     * @param bool $static
     * @return CurlWrapper
     * @since 6.0
     */
    public function setCurlHeader($key = '', $value = '', $static = false)
    {
        if (is_array($key) && empty($value)) {
            // Handle as bulk if this request (for example) comes from NetWrapper.
            foreach ($key as $getKey => $getValue) {
                $this->setCurlHeader($getKey, $getValue);
            }
            return $this;
        }

        if (!empty($key)) {
            if (!is_array($key)) {
                $this->customPreHeaders[$key] = $value;
                if ($static) {
                    $this->customPreHeadersStatic[$key] = $value;
                }
            } else {
                foreach ($key as $arrayKey => $arrayValue) {
                    $this->customPreHeaders[$arrayKey] = $arrayValue;
                    if ($static) {
                        $this->customPreHeadersStatic[$arrayKey] = $arrayValue;
                    }
                }
            }
        }

        return $this;
    }

    /**
     * Same as setCurlHeader but streamlined compatibility.
     *
     * @param string $key
     * @param string $value
     * @param false $static
     * @return $this
     * @since 6.1.2
     */
    public function setHeader($key = '', $value = '', $static = false)
    {
        return $this->setCurlHeader($key, $value, $static);
    }

    /**
     * @param $proxyAddress
     * @param int $proxyType Default: 0 = HTTP
     * @return CurlWrapper
     * @since 6.1.0
     */
    public function setProxy($proxyAddress, $proxyType = 0)
    {
        $this->CONFIG->setOption(WrapperCurlOpt::NETCURL_CURLOPT_PROXY, $proxyAddress);
        $this->CONFIG->setOption(WrapperCurlOpt::NETCURL_CURLOPT_PROXYTYPE, $proxyType);

        return $this;
    }

    /**
     * Enable instant exceptions on curl_multi errors.
     * @param bool $throwInstant
     * @return CurlWrapper
     * @since 6.1.0
     */
    public function setCurlMultiInstantException($throwInstant = true)
    {
        $this->instantCurlMultiErrors = $throwInstant;
        return $this;
    }

    /**
     * @return WrapperConfig
     * @since 6.1.0
     */
    public function getConfig()
    {
        return $this->CONFIG;
    }

    /**
     * @param WrapperConfig $config
     * @return CurlWrapper
     * @since 6.1.0
     */
    public function setConfig($config)
    {
        $this->CONFIG = $this->getInheritedConfig($config);

        return $this;
    }

    /**
     * @param $config
     * @return mixed
     * @since 6.1.0
     */
    private function getInheritedConfig($config)
    {
        $config->setCurrentWrapper($this->CONFIG->getCurrentWrapper());

        return $config;
    }

    /**
     * @param $username
     * @param $password
     * @param int $authType
     * @return CurlWrapper
     * @since 6.1.0
     */
    public function setAuthentication($username, $password, $authType = authType::BASIC)
    {
        $this->CONFIG->setAuthentication($username, $password, $authType);

        return $this;
    }

    /**
     * @param string $url
     * @return int
     * @throws ExceptionHandler
     * @since 6.0
     */
    public function getCode($url = '')
    {
        if (is_array($this->curlMultiResponse) &&
            count($this->curlMultiResponse) === 1
        ) {
            $url = (string)key($this->curlMultiResponse);
        }

        if (!$this->isCurlMulti) {
            $return = $this->curlHttpCode;
        } elseif (isset($this->curlMultiHttpCode[$url])) {
            $return = $this->curlMultiHttpCode[$url];
        } else {
            if (empty($url)) {
                throw new ExceptionHandler(
                    sprintf(
                        'Can not use %s without an url in a curl_multi request.',
                        __FUNCTION__
                    )
                );
            }
            $return = $this->curlMultiHttpCode;
        }

        return $return;
    }

    /**
     * Get parsed response. No longer using IO.
     *
     * @param string $url
     * @return mixed
     * @throws ExceptionHandler
     * @since 6.0
     */
    public function getParsed($url = '')
    {
        if (is_array($this->curlMultiResponse) && count($this->curlMultiResponse) === 1) {
            $url = (string)key($this->curlMultiResponse);
        }

        return GenericParser::getParsed(
            $this->getBody($url),
            $this->getHeader('content-type', $url)
        );
    }

    /**
     * @param string $url
     * @return mixed
     * @throws ExceptionHandler
     * @since 6.0
     */
    public function getBody($url = '')
    {
        if (is_array($this->curlMultiResponse) && count($this->curlMultiResponse) === 1) {
            $url = (string)key($this->curlMultiResponse);
        }

        if (!$this->isCurlMulti) {
            $return = $this->curlResponse;
        } elseif (isset($this->curlMultiResponse[$url])) {
            $return = $this->curlMultiResponse[$url];
        } else {
            // Sort out one request.
            if (empty($url)) {
                throw new ExceptionHandler(
                    sprintf(
                        'Can not use %s without an url in a curl_multi request.',
                        __FUNCTION__
                    )
                );
            }
            $return = $this->curlMultiResponse;
        }

        return $return;
    }

    /**
     * @param string $specificKey
     * @param string $specificUrl
     * @return string
     * @throws ExceptionHandler
     * @since 6.0
     */
    public function getHeader($specificKey = '', $specificUrl = '')
    {
        $return = [];

        $headerRequest = is_array($this->curlResponseHeaders) ? $this->curlResponseHeaders : [];

        if ($this->isCurlMulti) {
            if (is_array($this->curlResponseHeaders) && count($this->curlResponseHeaders) === 1) {
                $headerRequest = array_pop($this->curlResponseHeaders);
            } else {
                if (empty($specificUrl)) {
                    throw new ExceptionHandler(
                        'You must specify the URL from which you want to retrieve headers.',
                        Constants::LIB_MULTI_HEADER
                    );
                }
                $headerRequest = isset($this->curlResponseHeaders[$specificUrl]) &&
                is_array($this->curlResponseHeaders[$specificUrl]) ? $this->curlResponseHeaders[$specificUrl] : [];
            }
        }

        if (is_array($headerRequest) && count($headerRequest)) {
            foreach ($headerRequest as $headKey => $headArray) {
                // Something has pushed in duplicates of a header row, so lets pop one.
                if (count($headArray) > 1) {
                    $headArray = array_pop($headArray);
                }
                if (is_array($headArray) && count($headArray) === 1) {
                    if (!$specificKey) {
                        $return[] = sprintf("%s: %s", $headKey, array_pop($headArray));
                    } elseif (strtolower($specificKey) === strtolower($headKey)) {
                        $return[] = sprintf("%s", array_pop($headArray));
                    } elseif (strtolower($specificKey) === 'http') {
                        if (0 === stripos($headKey, "http")) {
                            $return[] = sprintf("%s", array_pop($headArray));
                        }
                    }
                }
            }
        }

        return implode("\n", $return);
    }

    /**
     * @param string $url
     * @param array $data
     * @param int $method
     * @param int $dataType
     * @return $this
     * @throws ExceptionHandler
     * @since 6.1.0
     */
    public function request($url = '', $data = [], $method = requestMethod::METHOD_GET, $dataType = dataType::NORMAL)
    {
        $this->CONFIG->request($url, $data, $method, $dataType);
        $this->getCurlRequest();

        return $this;
    }

    /**
     * The curl_exec part.
     * @return $this
     * @throws ExceptionHandler
     * @since 6.1.0
     */
    public function getCurlRequest()
    {
        $this->resetCurlRequest();
        $this->initCurlHandle();

        if (!$this->isCurlMulti && $this->isCurlResource($this->getCurlHandle())) {
            $this->curlResponse = curl_exec($this->curlHandle);
            // Friendly anti-backfire support.
            $this->curlHttpCode = curl_getinfo(
                $this->curlHandle,
                defined('CURLINFO_RESPONSE_CODE') ? CURLINFO_RESPONSE_CODE : 2097154
            );
            $this->getCurlException($this->curlHandle, $this->curlHttpCode);
        } elseif ($this->isCurlResource($this->curlMultiHandle)) {
            $this->curlMultiResponse = $this->getCurlMultiRequest();
            $this->getCurlExceptions();
        }

        return $this;
    }

    /**
     * From PHP 8.0 curl are returned as objects instead of resources (like CurlHandle, CurlMultiHandle, etc).
     * When investigation started, I did not know how much things was affected so this method was written to
     * easier change future changes.
     * @param $resource
     * @return bool
     * @since 6.1.1
     */
    private function isCurlResource($resource)
    {
        $return = !empty($resource);
        if ($this->strictResource) {
            $return = is_resource($resource) || is_object($resource);
        }

        return $return;
    }

    /**
     * Returns simple curl handle only.
     *
     * @return resource
     * @throws ExceptionHandler
     * @since 6.1.0
     */
    public function getCurlHandle()
    {
        $return = null;
        if ($this->isCurlResource($this->curlHandle)) {
            $return = $this->curlHandle;
        } elseif ($this->isCurlResource($this->curlMultiHandle) && count($this->curlMultiHandleObjects)) {
            $return = $this->curlMultiHandle;
        } else {
            $return = $this->initCurlHandle()->getCurlHandle();
        }

        return $return;
    }

    /**
     * @param $curlHandle
     * @param $httpCode
     * @return CurlWrapper
     * @throws ExceptionHandler
     * @since 6.1.0
     */
    private function getCurlException($curlHandle, $httpCode)
    {
        $errorString = curl_error($curlHandle);
        $errorCode = curl_errno($curlHandle);
        if ($errorCode) {
            throw new ExceptionHandler(
                sprintf(
                    'curl error (%s): %s',
                    $errorCode,
                    $errorString
                ),
                $errorCode,
                null,
                null,
                null,
                $this
            );
        }

        $httpHead = $this->getHeader('http');
        if (empty($errorString) && !empty($httpHead)) {
            $errorString = $httpHead;
        }
        $this->CONFIG->getHttpException($errorString, $httpCode, null, $this);

        return $this;
    }

    /**
     * @return array
     * @throws ExceptionHandler
     * @since 6.1.0
     */
    private function getCurlMultiRequest()
    {
        $return = [];

        do {
            $status = curl_multi_exec($this->curlMultiHandle, $active);
            if ($active) {
                curl_multi_select($this->curlMultiHandle);
            }
        } while ($active && $status === CURLM_OK);

        foreach ($this->curlMultiHandleObjects as $url => $curlHandleObject) {
            $return[$url] = curl_multi_getcontent($curlHandleObject);
            $this->curlMultiHttpCode[$url] = GenericParser::getHttpHead($this->getHeader('http', $url));
            $this->getCurlMultiErrors($curlHandleObject, $url);
            curl_multi_remove_handle($this->curlMultiHandle, $curlHandleObject);
        }

        return $return;
    }

    /**
     * Get errors from a curl_multi handle.
     * Use getCurlException in future, if possible.
     *
     * @param $curlMultiHandle
     * @param $url
     * @return CurlWrapper
     * @throws ExceptionHandler
     * @since 6.1.0
     */
    private function getCurlMultiErrors($curlMultiHandle, $url)
    {
        $internalCurlErrorCode = curl_errno($curlMultiHandle);
        $internalCurlErrorMessage = curl_error($curlMultiHandle);

        $curlHttpDataCode = GenericParser::getHttpHead($this->getHeader('http', $url));
        $curlHttpDataMessage = GenericParser::getHttpHead($this->getHeader('http', $url), 'message');

        if ($internalCurlErrorCode) {
            try {
                $this->CONFIG->getHttpException(
                    $internalCurlErrorMessage,
                    $internalCurlErrorCode,
                    null,
                    $this,
                    true
                );
            } catch (ExceptionHandler $curlMultiException) {
                $this->curlMultiErrors[$url] = $curlMultiException;
                // If instant curl errors are requested, throw on first error.
                if ($this->instantCurlMultiErrors) {
                    throw $curlMultiException;
                }
            }
        }

        try {
            $this->CONFIG->getHttpException(
                $curlHttpDataMessage,
                $curlHttpDataCode,
                null,
                $this
            );
        } catch (ExceptionHandler $curlMultiException) {
            $this->curlMultiErrors[$url] = $curlMultiException;
            // If instant curl errors are requested, throw on first error.
            if ($this->instantCurlMultiErrors) {
                throw $curlMultiException;
            }
        }

        return $this;
    }

    /**
     * @return $this
     * @throws ExceptionHandler
     * @since 6.1.0
     */
    public function getCurlExceptions()
    {
        if (is_array($this->curlMultiErrors)) {
            if (count($this->curlMultiErrors) === 1) {
                /** @var ExceptionHandler $exceptionHandler */
                $exceptionHandler = array_pop($this->curlMultiErrors);
                throw new ExceptionHandler(
                    sprintf(
                        'curl_multi request found one error in %s: %s',
                        key($this->curlMultiErrors),
                        $exceptionHandler->getMessage()
                    ),
                    $exceptionHandler->getCode(),
                    $exceptionHandler,
                    null,
                    null,
                    $this
                );
            }

            if (count($this->curlMultiErrors) > 1) {
                throw new ExceptionHandler(
                    'Multiple errors discovered in curl_multi request. Details are attached to this ExceptionHandler.',
                    Constants::LIB_NETCURL_CURL_MULTI_EXCEPTION_DISCOVERY,
                    null,
                    null,
                    null,
                    $this
                );
            }
        }

        return $this;
    }

    /**
     * @param $curlHandle
     * @param $header
     * @return int
     * @since 6.1.0
     * @noinspection PhpUnusedPrivateMethodInspection
     */
    private function getCurlHeaderRow($curlHandle, $header)
    {
        $headSplit = explode(':', $header, 2);
        $spacedSplit = explode(' ', $header, 2);

        if (count($headSplit) < 2) {
            if (count($spacedSplit) > 1) {
                if (!$this->isCurlMulti) {
                    $this->curlResponseHeaders[$spacedSplit[0]][] = trim($spacedSplit[1]);
                } else {
                    $urlinfo = curl_getinfo($curlHandle, CURLINFO_EFFECTIVE_URL);
                    $this->curlResponseHeaders[$urlinfo][$spacedSplit[0]][] = trim($spacedSplit[1]);
                }
            }
            return strlen($header);
        }

        if (!$this->isCurlMulti) {
            $this->curlResponseHeaders[$headSplit[0]][] = trim($headSplit[1]);
        } else {
            $urlinfo = curl_getinfo($curlHandle, CURLINFO_EFFECTIVE_URL);
            if (!isset($this->curlResponseHeaders[$urlinfo])) {
                $this->curlResponseHeaders[$urlinfo] = [];
            }
            $this->curlResponseHeaders[$urlinfo][$headSplit[0]][] = trim($headSplit[1]);
        }

        return strlen($header);
    }

    /**
     * @param $name
     * @param $arguments
     * @return mixed
     * @throws ExceptionHandler
     * @since 6.1.2
     */
    public function __call($name, $arguments)
    {
        $return = null;

        $compatibilityMethods = $this->CONFIG->getCompatibilityMethods();
        if (isset($compatibilityMethods[$name])) {
            $name = $compatibilityMethods[$name];
            $return = call_user_func_array([$this, $name], $arguments);
        }

        if (!is_null($return)) {
            return $return;
        }
        throw new ExceptionHandler(
            sprintf('Function "%s" not available.', $name),
            Constants::LIB_METHOD_OR_LIBRARY_UNAVAILABLE
        );
    }


    /**
     * @param $url
     * @throws ExceptionHandler
     * @since 6.1.0
     * @noinspection PhpUnusedPrivateMethodInspection
     */
    private function throwExceptionInvalidUrl($url)
    {
        if (!empty($url)) {
            throw new ExceptionHandler(
                sprintf(
                    '%s is not a valid URL.',
                    $url
                ),
                Constants::LIB_INVALID_URL
            );
        }

        throw new ExceptionHandler(
            'URL must not be empty.',
            Constants::LIB_EMPTY_URL
        );
    }
}