From: Philipp Date: Sat, 23 Oct 2021 10:50:31 +0000 (+0200) Subject: Restructure HTTPClient for new paradigm X-Git-Url: https://git.mxchange.org/?a=commitdiff_plain;h=409d909d0f52395eae4bebf7960c368efed3865f;p=friendica.git Restructure HTTPClient for new paradigm --- diff --git a/src/Content/Text/BBCode.php b/src/Content/Text/BBCode.php index da95c3d673..ecaf345274 100644 --- a/src/Content/Text/BBCode.php +++ b/src/Content/Text/BBCode.php @@ -39,7 +39,7 @@ use Friendica\Model\Event; use Friendica\Model\Photo; use Friendica\Model\Post; use Friendica\Model\Tag; -use Friendica\Network\HTTPClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Object\Image; use Friendica\Protocol\Activity; use Friendica\Util\Images; @@ -1201,7 +1201,7 @@ class BBCode $text = DI::cache()->get($cache_key); if (is_null($text)) { - $curlResult = DI::httpClient()->head($match[1], [HTTPClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]); + $curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]); if ($curlResult->isSuccess()) { $mimetype = $curlResult->getHeader('Content-Type')[0] ?? ''; } else { @@ -1272,7 +1272,7 @@ class BBCode return $text; } - $curlResult = DI::httpClient()->head($match[1], [HTTPClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]); + $curlResult = DI::httpClient()->head($match[1], [HttpClientOptions::TIMEOUT => DI::config()->get('system', 'xrd_timeout')]); if ($curlResult->isSuccess()) { $mimetype = $curlResult->getHeader('Content-Type')[0] ?? ''; } else { diff --git a/src/Core/Search.php b/src/Core/Search.php index ff4abdfacf..82c0ea3d36 100644 --- a/src/Core/Search.php +++ b/src/Core/Search.php @@ -24,7 +24,7 @@ namespace Friendica\Core; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Network\HTTPException; -use Friendica\Network\HTTPClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Object\Search\ContactResult; use Friendica\Object\Search\ResultList; use Friendica\Util\Network; @@ -228,7 +228,7 @@ class Search $return = Contact::searchByName($search, $mode); } else { $p = $page > 1 ? 'p=' . $page : ''; - $curlResult = DI::httpClient()->get(self::getGlobalDirectory() . '/search/people?' . $p . '&q=' . urlencode($search), [HTTPClientOptions::ACCEPT_CONTENT => ['application/json']]); + $curlResult = DI::httpClient()->get(self::getGlobalDirectory() . '/search/people?' . $p . '&q=' . urlencode($search), [HttpClientOptions::ACCEPT_CONTENT => ['application/json']]); if ($curlResult->isSuccess()) { $searchResult = json_decode($curlResult->getBody(), true); if (!empty($searchResult['profiles'])) { diff --git a/src/DI.php b/src/DI.php index 8ee7004408..ecc65bbc98 100644 --- a/src/DI.php +++ b/src/DI.php @@ -415,11 +415,11 @@ abstract class DI // /** - * @return Network\IHTTPClient + * @return Network\HTTPClient\Capability\ICanRequestPerHttp */ public static function httpClient() { - return self::$dice->create(Network\IHTTPClient::class); + return self::$dice->create(Network\HTTPClient\Capability\ICanRequestPerHttp::class); } // diff --git a/src/Factory/HTTPClientFactory.php b/src/Factory/HTTPClientFactory.php deleted file mode 100644 index 18e2b0f553..0000000000 --- a/src/Factory/HTTPClientFactory.php +++ /dev/null @@ -1,113 +0,0 @@ -config = $config; - $this->profiler = $profiler; - $this->baseUrl = $baseUrl; - } - - /** - * Creates a IHTTPClient for communications with HTTP endpoints - * - * @param HandlerStack|null $handlerStack (optional) A handler replacement (just usefull at test environments) - * - * @return IHTTPClient - */ - public function createClient(HandlerStack $handlerStack = null): IHTTPClient - { - $proxy = $this->config->get('system', 'proxy'); - - if (!empty($proxy)) { - $proxyuser = $this->config->get('system', 'proxyuser'); - - if (!empty($proxyuser)) { - $proxy = $proxyuser . '@' . $proxy; - } - } - - $logger = $this->logger; - - $onRedirect = function ( - RequestInterface $request, - ResponseInterface $response, - UriInterface $uri - ) use ($logger) { - $logger->notice('Curl redirect.', ['url' => $request->getUri(), 'to' => $uri, 'method' => $request->getMethod()]); - }; - - $userAgent = FRIENDICA_PLATFORM . " '" . - FRIENDICA_CODENAME . "' " . - FRIENDICA_VERSION . '-' . - DB_UPDATE_VERSION . '; ' . - $this->baseUrl->get(); - - $guzzle = new Client([ - RequestOptions::ALLOW_REDIRECTS => [ - 'max' => 8, - 'on_redirect' => $onRedirect, - 'track_redirect' => true, - 'strict' => true, - 'referer' => true, - ], - RequestOptions::HTTP_ERRORS => false, - // Without this setting it seems as if some webservers send compressed content - // This seems to confuse curl so that it shows this uncompressed. - /// @todo We could possibly set this value to "gzip" or something similar - RequestOptions::DECODE_CONTENT => '', - RequestOptions::FORCE_IP_RESOLVE => ($this->config->get('system', 'ipv4_resolve') ? 'v4' : null), - RequestOptions::CONNECT_TIMEOUT => 10, - RequestOptions::TIMEOUT => $this->config->get('system', 'curl_timeout', 60), - // by default we will allow self-signed certs - // but it can be overridden - RequestOptions::VERIFY => (bool)$this->config->get('system', 'verifyssl'), - RequestOptions::PROXY => $proxy, - RequestOptions::HEADERS => [ - 'User-Agent' => $userAgent, - ], - 'handler' => $handlerStack ?? HandlerStack::create(), - ]); - - $resolver = new URLResolver(); - $resolver->setUserAgent($userAgent); - $resolver->setMaxRedirects(10); - $resolver->setRequestTimeout(10); - // if the file is too large then exit - $resolver->setMaxResponseDataSize(1000000); - // Designate a temporary file that will store cookies during the session. - // Some websites test the browser for cookie support, so this enhances results. - $resolver->setCookieJar(get_temppath() .'/resolver-cookie-' . Strings::getRandomName(10)); - - return new HTTPClient($logger, $this->profiler, $guzzle, $resolver); - } -} diff --git a/src/Model/GServer.php b/src/Model/GServer.php index 7c5a2b98a3..c56e7701e0 100644 --- a/src/Model/GServer.php +++ b/src/Model/GServer.php @@ -32,8 +32,8 @@ use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Module\Register; -use Friendica\Network\HTTPClientOptions; -use Friendica\Network\IHTTPResult; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; +use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses; use Friendica\Protocol\Relay; use Friendica\Util\DateTimeFormat; use Friendica\Util\Network; @@ -315,7 +315,7 @@ class GServer // When a nodeinfo is present, we don't need to dig further $xrd_timeout = DI::config()->get('system', 'xrd_timeout'); - $curlResult = DI::httpClient()->get($url . '/.well-known/nodeinfo', [HTTPClientOptions::TIMEOUT => $xrd_timeout]); + $curlResult = DI::httpClient()->get($url . '/.well-known/nodeinfo', [HttpClientOptions::TIMEOUT => $xrd_timeout]); if ($curlResult->isTimeout()) { self::setFailure($url); return false; @@ -323,7 +323,7 @@ class GServer // On a redirect follow the new host but mark the old one as failure if ($curlResult->isSuccess() && (parse_url($url, PHP_URL_HOST) != parse_url($curlResult->getRedirectUrl(), PHP_URL_HOST))) { - $curlResult = DI::httpClient()->get($url, [HTTPClientOptions::TIMEOUT => $xrd_timeout]); + $curlResult = DI::httpClient()->get($url, [HttpClientOptions::TIMEOUT => $xrd_timeout]); if (parse_url($url, PHP_URL_HOST) != parse_url($curlResult->getRedirectUrl(), PHP_URL_HOST)) { Logger::info('Found redirect. Mark old entry as failure', ['old' => $url, 'new' => $curlResult->getRedirectUrl()]); self::setFailure($url); @@ -359,7 +359,7 @@ class GServer $basedata = ['detection-method' => self::DETECT_MANUAL]; } - $curlResult = DI::httpClient()->get($baseurl, [HTTPClientOptions::TIMEOUT => $xrd_timeout]); + $curlResult = DI::httpClient()->get($baseurl, [HttpClientOptions::TIMEOUT => $xrd_timeout]); if ($curlResult->isSuccess()) { if ((parse_url($baseurl, PHP_URL_HOST) != parse_url($curlResult->getRedirectUrl(), PHP_URL_HOST))) { Logger::info('Found redirect. Mark old entry as failure', ['old' => $url, 'new' => $curlResult->getRedirectUrl()]); @@ -383,7 +383,7 @@ class GServer // When the base path doesn't seem to contain a social network we try the complete path. // Most detectable system have to be installed in the root directory. // We checked the base to avoid false positives. - $curlResult = DI::httpClient()->get($url, [HTTPClientOptions::TIMEOUT => $xrd_timeout]); + $curlResult = DI::httpClient()->get($url, [HttpClientOptions::TIMEOUT => $xrd_timeout]); if ($curlResult->isSuccess()) { $urldata = self::analyseRootHeader($curlResult, $serverdata); $urldata = self::analyseRootBody($curlResult, $urldata, $url); @@ -672,13 +672,13 @@ class GServer /** * Detect server type by using the nodeinfo data * - * @param string $url address of the server - * @param IHTTPResult $httpResult + * @param string $url address of the server + * @param ICanHandleHttpResponses $httpResult * * @return array Server data * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ - private static function fetchNodeinfo(string $url, IHTTPResult $httpResult) + private static function fetchNodeinfo(string $url, ICanHandleHttpResponses $httpResult) { if (!$httpResult->isSuccess()) { return []; @@ -959,7 +959,7 @@ class GServer private static function validHostMeta(string $url) { $xrd_timeout = DI::config()->get('system', 'xrd_timeout'); - $curlResult = DI::httpClient()->get($url . '/.well-known/host-meta', [HTTPClientOptions::TIMEOUT => $xrd_timeout]); + $curlResult = DI::httpClient()->get($url . '/.well-known/host-meta', [HttpClientOptions::TIMEOUT => $xrd_timeout]); if (!$curlResult->isSuccess()) { return false; } @@ -1725,7 +1725,7 @@ class GServer if (!empty($accesstoken)) { $api = 'https://instances.social/api/1.0/instances/list?count=0'; - $curlResult = DI::httpClient()->get($api, [HTTPClientOptions::HEADERS => ['Authorization' => ['Bearer ' . $accesstoken]]]); + $curlResult = DI::httpClient()->get($api, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . $accesstoken]]]); if ($curlResult->isSuccess()) { $servers = json_decode($curlResult->getBody(), true); diff --git a/src/Model/Post/Link.php b/src/Model/Post/Link.php index dcda036ec1..50ed12a369 100644 --- a/src/Model/Post/Link.php +++ b/src/Model/Post/Link.php @@ -26,7 +26,7 @@ use Friendica\Core\System; use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\DI; -use Friendica\Network\HTTPClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Util\Proxy; /** @@ -100,7 +100,7 @@ class Link { $timeout = DI::config()->get('system', 'xrd_timeout'); - $curlResult = DI::httpClient()->head($url, [HTTPClientOptions::TIMEOUT => $timeout]); + $curlResult = DI::httpClient()->head($url, [HttpClientOptions::TIMEOUT => $timeout]); if ($curlResult->isSuccess()) { if (empty($media['mimetype'])) { return $curlResult->getHeader('Content-Type')[0] ?? ''; diff --git a/src/Model/Post/Media.php b/src/Model/Post/Media.php index b42ba89cf4..6fe996d76a 100644 --- a/src/Model/Post/Media.php +++ b/src/Model/Post/Media.php @@ -30,7 +30,7 @@ use Friendica\DI; use Friendica\Model\Item; use Friendica\Model\Photo; use Friendica\Model\Post; -use Friendica\Network\HTTPClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Util\Images; use Friendica\Util\Network; use Friendica\Util\ParseUrl; @@ -168,7 +168,7 @@ class Media // Fetch the mimetype or size if missing. if (empty($media['mimetype']) || empty($media['size'])) { $timeout = DI::config()->get('system', 'xrd_timeout'); - $curlResult = DI::httpClient()->head($media['url'], [HTTPClientOptions::TIMEOUT => $timeout]); + $curlResult = DI::httpClient()->head($media['url'], [HttpClientOptions::TIMEOUT => $timeout]); if ($curlResult->isSuccess()) { if (empty($media['mimetype'])) { $media['mimetype'] = $curlResult->getHeader('Content-Type')[0] ?? ''; diff --git a/src/Module/Magic.php b/src/Module/Magic.php index d508835b02..c51c05844c 100644 --- a/src/Module/Magic.php +++ b/src/Module/Magic.php @@ -28,7 +28,7 @@ use Friendica\Database\DBA; use Friendica\DI; use Friendica\Model\Contact; use Friendica\Model\User; -use Friendica\Network\HTTPClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Util\HTTPSignature; use Friendica\Util\Strings; @@ -102,7 +102,7 @@ class Magic extends BaseModule ); // Try to get an authentication token from the other instance. - $curlResult = DI::httpClient()->get($basepath . '/owa', [HTTPClientOptions::HEADERS => $header]); + $curlResult = DI::httpClient()->get($basepath . '/owa', [HttpClientOptions::HEADERS => $header]); if ($curlResult->isSuccess()) { $j = json_decode($curlResult->getBody(), true); diff --git a/src/Network/CurlResult.php b/src/Network/CurlResult.php deleted file mode 100644 index 8b3b12876a..0000000000 --- a/src/Network/CurlResult.php +++ /dev/null @@ -1,348 +0,0 @@ -. - * - */ - -namespace Friendica\Network; - -use Friendica\Core\Logger; -use Friendica\Core\System; -use Friendica\Network\HTTPException\InternalServerErrorException; -use Friendica\Util\Network; - -/** - * A content class for Curl call results - */ -class CurlResult implements IHTTPResult -{ - /** - * @var int HTTP return code or 0 if timeout or failure - */ - private $returnCode; - - /** - * @var string the content type of the Curl call - */ - private $contentType; - - /** - * @var string the HTTP headers of the Curl call - */ - private $header; - - /** - * @var array the HTTP headers of the Curl call - */ - private $header_fields; - - /** - * @var boolean true (if HTTP 2xx result) or false - */ - private $isSuccess; - - /** - * @var string the URL which was called - */ - private $url; - - /** - * @var string in case of redirect, content was finally retrieved from this URL - */ - private $redirectUrl; - - /** - * @var string fetched content - */ - private $body; - - /** - * @var array some informations about the fetched data - */ - private $info; - - /** - * @var boolean true if the URL has a redirect - */ - private $isRedirectUrl; - - /** - * @var boolean true if the curl request timed out - */ - private $isTimeout; - - /** - * @var int the error number or 0 (zero) if no error - */ - private $errorNumber; - - /** - * @var string the error message or '' (the empty string) if no - */ - private $error; - - /** - * Creates an errored CURL response - * - * @param string $url optional URL - * - * @return IHTTPResult a CURL with error response - * @throws InternalServerErrorException - */ - public static function createErrorCurl($url = '') - { - return new CurlResult($url, '', ['http_code' => 0]); - } - - /** - * Curl constructor. - * @param string $url the URL which was called - * @param string $result the result of the curl execution - * @param array $info an additional info array - * @param int $errorNumber the error number or 0 (zero) if no error - * @param string $error the error message or '' (the empty string) if no - * - * @throws InternalServerErrorException when HTTP code of the CURL response is missing - */ - public function __construct($url, $result, $info, $errorNumber = 0, $error = '') - { - if (!array_key_exists('http_code', $info)) { - throw new InternalServerErrorException('CURL response doesn\'t contains a response HTTP code'); - } - - $this->returnCode = $info['http_code']; - $this->url = $url; - $this->info = $info; - $this->errorNumber = $errorNumber; - $this->error = $error; - - Logger::debug('construct', ['url' => $url, 'returncode' => $this->returnCode, 'result' => $result]); - - $this->parseBodyHeader($result); - $this->checkSuccess(); - $this->checkRedirect(); - $this->checkInfo(); - } - - private function parseBodyHeader($result) - { - // Pull out multiple headers, e.g. proxy and continuation headers - // allow for HTTP/2.x without fixing code - - $header = ''; - $base = $result; - while (preg_match('/^HTTP\/.+? \d+/', $base)) { - $chunk = substr($base, 0, strpos($base, "\r\n\r\n") + 4); - $header .= $chunk; - $base = substr($base, strlen($chunk)); - } - - $this->body = substr($result, strlen($header)); - $this->header = $header; - $this->header_fields = []; // Is filled on demand - } - - private function checkSuccess() - { - $this->isSuccess = ($this->returnCode >= 200 && $this->returnCode <= 299) || $this->errorNumber == 0; - - // Everything higher or equal 400 is not a success - if ($this->returnCode >= 400) { - $this->isSuccess = false; - } - - if (!$this->isSuccess) { - Logger::debug('debug', ['info' => $this->info]); - } - - if (!$this->isSuccess && $this->errorNumber == CURLE_OPERATION_TIMEDOUT) { - $this->isTimeout = true; - } else { - $this->isTimeout = false; - } - } - - private function checkRedirect() - { - if (!array_key_exists('url', $this->info)) { - $this->redirectUrl = ''; - } else { - $this->redirectUrl = $this->info['url']; - } - - if ($this->returnCode == 301 || $this->returnCode == 302 || $this->returnCode == 303 || $this->returnCode== 307) { - $redirect_parts = parse_url($this->info['redirect_url'] ?? ''); - if (empty($redirect_parts)) { - $redirect_parts = []; - } - - if (preg_match('/(Location:|URI:)(.*?)\n/i', $this->header, $matches)) { - $redirect_parts2 = parse_url(trim(array_pop($matches))); - if (!empty($redirect_parts2)) { - $redirect_parts = array_merge($redirect_parts, $redirect_parts2); - } - } - - $parts = parse_url($this->info['url'] ?? ''); - if (empty($parts)) { - $parts = []; - } - - /// @todo Checking the corresponding RFC which parts of a redirect can be ommitted. - $components = ['scheme', 'host', 'path', 'query', 'fragment']; - foreach ($components as $component) { - if (empty($redirect_parts[$component]) && !empty($parts[$component])) { - $redirect_parts[$component] = $parts[$component]; - } - } - - $this->redirectUrl = Network::unparseURL($redirect_parts); - - $this->isRedirectUrl = true; - } else { - $this->isRedirectUrl = false; - } - } - - private function checkInfo() - { - if (isset($this->info['content_type'])) { - $this->contentType = $this->info['content_type']; - } else { - $this->contentType = ''; - } - } - - /** {@inheritDoc} */ - public function getReturnCode() - { - return $this->returnCode; - } - - /** {@inheritDoc} */ - public function getContentType() - { - return $this->contentType; - } - - /** {@inheritDoc} */ - public function getHeader($header) - { - if (empty($header)) { - return []; - } - - $header = strtolower(trim($header)); - - $headers = $this->getHeaderArray(); - - if (isset($headers[$header])) { - return $headers[$header]; - } - - return []; - } - - /** {@inheritDoc} */ - public function getHeaders() - { - return $this->getHeaderArray(); - } - - /** {@inheritDoc} */ - public function inHeader(string $field) - { - $field = strtolower(trim($field)); - - $headers = $this->getHeaderArray(); - - return array_key_exists($field, $headers); - } - - /** {@inheritDoc} */ - public function getHeaderArray() - { - if (!empty($this->header_fields)) { - return $this->header_fields; - } - - $this->header_fields = []; - - $lines = explode("\n", trim($this->header)); - foreach ($lines as $line) { - $parts = explode(':', $line); - $headerfield = strtolower(trim(array_shift($parts))); - $headerdata = trim(implode(':', $parts)); - if (empty($this->header_fields[$headerfield])) { - $this->header_fields[$headerfield] = [$headerdata]; - } elseif (!in_array($headerdata, $this->header_fields[$headerfield])) { - $this->header_fields[$headerfield][] = $headerdata; - } - } - - return $this->header_fields; - } - - /** {@inheritDoc} */ - public function isSuccess() - { - return $this->isSuccess; - } - - /** {@inheritDoc} */ - public function getUrl() - { - return $this->url; - } - - /** {@inheritDoc} */ - public function getRedirectUrl() - { - return $this->redirectUrl; - } - - /** {@inheritDoc} */ - public function getBody() - { - return $this->body; - } - - /** {@inheritDoc} */ - public function isRedirectUrl() - { - return $this->isRedirectUrl; - } - - /** {@inheritDoc} */ - public function getErrorNumber() - { - return $this->errorNumber; - } - - /** {@inheritDoc} */ - public function getError() - { - return $this->error; - } - - /** {@inheritDoc} */ - public function isTimeout() - { - return $this->isTimeout; - } -} diff --git a/src/Network/GuzzleResponse.php b/src/Network/GuzzleResponse.php deleted file mode 100644 index d5afb4ca6d..0000000000 --- a/src/Network/GuzzleResponse.php +++ /dev/null @@ -1,153 +0,0 @@ -. - * - */ - -namespace Friendica\Network; - -use Friendica\Core\Logger; -use Friendica\Core\System; -use Friendica\Network\HTTPException\NotImplementedException; -use GuzzleHttp\Psr7\Response; -use Psr\Http\Message\ResponseInterface; - -/** - * A content wrapper class for Guzzle call results - */ -class GuzzleResponse extends Response implements IHTTPResult, ResponseInterface -{ - /** @var string The URL */ - private $url; - /** @var boolean */ - private $isTimeout; - /** @var boolean */ - private $isSuccess; - /** - * @var int the error number or 0 (zero) if no error - */ - private $errorNumber; - - /** - * @var string the error message or '' (the empty string) if no - */ - private $error; - - public function __construct(ResponseInterface $response, string $url, $errorNumber = 0, $error = '') - { - parent::__construct($response->getStatusCode(), $response->getHeaders(), $response->getBody(), $response->getProtocolVersion(), $response->getReasonPhrase()); - $this->url = $url; - $this->error = $error; - $this->errorNumber = $errorNumber; - - $this->checkSuccess(); - } - - private function checkSuccess() - { - $this->isSuccess = ($this->getStatusCode() >= 200 && $this->getStatusCode() <= 299) || $this->errorNumber == 0; - - // Everything higher or equal 400 is not a success - if ($this->getReturnCode() >= 400) { - $this->isSuccess = false; - } - - if (!$this->isSuccess) { - Logger::debug('debug', ['info' => $this->getHeaders()]); - } - - if (!$this->isSuccess && $this->errorNumber == CURLE_OPERATION_TIMEDOUT) { - $this->isTimeout = true; - } else { - $this->isTimeout = false; - } - } - - /** {@inheritDoc} */ - public function getReturnCode() - { - return $this->getStatusCode(); - } - - /** {@inheritDoc} */ - public function getContentType() - { - $contentTypes = $this->getHeader('Content-Type') ?? []; - return array_pop($contentTypes) ?? ''; - } - - /** {@inheritDoc} */ - public function inHeader(string $field) - { - return $this->hasHeader($field); - } - - /** {@inheritDoc} */ - public function getHeaderArray() - { - return $this->getHeaders(); - } - - /** {@inheritDoc} */ - public function isSuccess() - { - return $this->isSuccess; - } - - /** {@inheritDoc} */ - public function getUrl() - { - return $this->url; - } - - /** {@inheritDoc} */ - public function getRedirectUrl() - { - return $this->url; - } - - /** {@inheritDoc} */ - public function isRedirectUrl() - { - throw new NotImplementedException(); - } - - /** {@inheritDoc} */ - public function getErrorNumber() - { - return $this->errorNumber; - } - - /** {@inheritDoc} */ - public function getError() - { - return $this->error; - } - - /** {@inheritDoc} */ - public function isTimeout() - { - return $this->isTimeout; - } - - /// @todo - fix mismatching use of "getBody()" as string here and parent "getBody()" as streaminterface - public function getBody(): string - { - return (string) parent::getBody(); - } -} diff --git a/src/Network/HTTPClient.php b/src/Network/HTTPClient.php deleted file mode 100644 index 004af57f9a..0000000000 --- a/src/Network/HTTPClient.php +++ /dev/null @@ -1,253 +0,0 @@ -. - * - */ - -namespace Friendica\Network; - -use Friendica\Core\System; -use Friendica\Util\Network; -use Friendica\Util\Profiler; -use GuzzleHttp\Client; -use GuzzleHttp\Cookie\FileCookieJar; -use GuzzleHttp\Exception\RequestException; -use GuzzleHttp\Exception\TransferException; -use GuzzleHttp\RequestOptions; -use mattwright\URLResolver; -use Psr\Http\Message\ResponseInterface; -use Psr\Log\InvalidArgumentException; -use Psr\Log\LoggerInterface; - -/** - * Performs HTTP requests to a given URL - */ -class HTTPClient implements IHTTPClient -{ - /** @var LoggerInterface */ - private $logger; - /** @var Profiler */ - private $profiler; - /** @var Client */ - private $client; - /** @var URLResolver */ - private $resolver; - - public function __construct(LoggerInterface $logger, Profiler $profiler, Client $client, URLResolver $resolver) - { - $this->logger = $logger; - $this->profiler = $profiler; - $this->client = $client; - $this->resolver = $resolver; - } - - /** - * {@inheritDoc} - */ - public function request(string $method, string $url, array $opts = []): IHTTPResult - { - $this->profiler->startRecording('network'); - $this->logger->debug('Request start.', ['url' => $url, 'method' => $method]); - - if (Network::isLocalLink($url)) { - $this->logger->info('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); - } - - if (strlen($url) > 1000) { - $this->logger->debug('URL is longer than 1000 characters.', ['url' => $url, 'callstack' => System::callstack(20)]); - $this->profiler->stopRecording(); - return CurlResult::createErrorCurl(substr($url, 0, 200)); - } - - $parts2 = []; - $parts = parse_url($url); - $path_parts = explode('/', $parts['path'] ?? ''); - foreach ($path_parts as $part) { - if (strlen($part) <> mb_strlen($part)) { - $parts2[] = rawurlencode($part); - } else { - $parts2[] = $part; - } - } - $parts['path'] = implode('/', $parts2); - $url = Network::unparseURL($parts); - - if (Network::isUrlBlocked($url)) { - $this->logger->info('Domain is blocked.', ['url' => $url]); - $this->profiler->stopRecording(); - return CurlResult::createErrorCurl($url); - } - - $conf = []; - - if (!empty($opts[HTTPClientOptions::COOKIEJAR])) { - $jar = new FileCookieJar($opts[HTTPClientOptions::COOKIEJAR]); - $conf[RequestOptions::COOKIES] = $jar; - } - - $headers = []; - - if (!empty($opts[HTTPClientOptions::ACCEPT_CONTENT])) { - $headers['Accept'] = $opts[HTTPClientOptions::ACCEPT_CONTENT]; - } - - if (!empty($opts[HTTPClientOptions::LEGACY_HEADER])) { - $this->logger->notice('Wrong option \'headers\' used.'); - $headers = array_merge($opts[HTTPClientOptions::LEGACY_HEADER], $headers); - } - - if (!empty($opts[HTTPClientOptions::HEADERS])) { - $headers = array_merge($opts[HTTPClientOptions::HEADERS], $headers); - } - - $conf[RequestOptions::HEADERS] = array_merge($this->client->getConfig(RequestOptions::HEADERS), $headers); - - if (!empty($opts[HTTPClientOptions::TIMEOUT])) { - $conf[RequestOptions::TIMEOUT] = $opts[HTTPClientOptions::TIMEOUT]; - } - - if (!empty($opts[HTTPClientOptions::BODY])) { - $conf[RequestOptions::BODY] = $opts[HTTPClientOptions::BODY]; - } - - if (!empty($opts[HTTPClientOptions::AUTH])) { - $conf[RequestOptions::AUTH] = $opts[HTTPClientOptions::AUTH]; - } - - $conf[RequestOptions::ON_HEADERS] = function (ResponseInterface $response) use ($opts) { - if (!empty($opts[HTTPClientOptions::CONTENT_LENGTH]) && - (int)$response->getHeaderLine('Content-Length') > $opts[HTTPClientOptions::CONTENT_LENGTH]) { - throw new TransferException('The file is too big!'); - } - }; - - try { - $this->logger->debug('http request config.', ['url' => $url, 'method' => $method, 'options' => $conf]); - - $response = $this->client->request($method, $url, $conf); - return new GuzzleResponse($response, $url); - } catch (TransferException $exception) { - if ($exception instanceof RequestException && - $exception->hasResponse()) { - return new GuzzleResponse($exception->getResponse(), $url, $exception->getCode(), ''); - } else { - return new CurlResult($url, '', ['http_code' => 500], $exception->getCode(), ''); - } - } catch (InvalidArgumentException | \InvalidArgumentException $argumentException) { - $this->logger->info('Invalid Argument for HTTP call.', ['url' => $url, 'method' => $method, 'exception' => $argumentException]); - return new CurlResult($url, '', ['http_code' => 500], $argumentException->getCode(), $argumentException->getMessage()); - } finally { - $this->logger->debug('Request stop.', ['url' => $url, 'method' => $method]); - $this->profiler->stopRecording(); - } - } - - /** {@inheritDoc} - */ - public function head(string $url, array $opts = []): IHTTPResult - { - return $this->request('head', $url, $opts); - } - - /** - * {@inheritDoc} - */ - public function get(string $url, array $opts = []): IHTTPResult - { - return $this->request('get', $url, $opts); - } - - /** - * {@inheritDoc} - */ - public function post(string $url, $params, array $headers = [], int $timeout = 0): IHTTPResult - { - $opts = []; - - $opts[HTTPClientOptions::BODY] = $params; - - if (!empty($headers)) { - $opts[HTTPClientOptions::HEADERS] = $headers; - } - - if (!empty($timeout)) { - $opts[HTTPClientOptions::TIMEOUT] = $timeout; - } - - return $this->request('post', $url, $opts); - } - - /** - * {@inheritDoc} - */ - public function finalUrl(string $url) - { - $this->profiler->startRecording('network'); - - if (Network::isLocalLink($url)) { - $this->logger->debug('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); - } - - if (Network::isUrlBlocked($url)) { - $this->logger->info('Domain is blocked.', ['url' => $url]); - return $url; - } - - if (Network::isRedirectBlocked($url)) { - $this->logger->info('Domain should not be redirected.', ['url' => $url]); - return $url; - } - - $url = Network::stripTrackingQueryParams($url); - - $url = trim($url, "'"); - - $urlResult = $this->resolver->resolveURL($url); - - if ($urlResult->didErrorOccur()) { - throw new TransferException($urlResult->getErrorMessageString(), $urlResult->getHTTPStatusCode()); - } - - return $urlResult->getURL(); - } - - /** - * {@inheritDoc} - */ - public function fetch(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = '') - { - $ret = $this->fetchFull($url, $timeout, $accept_content, $cookiejar); - - return $ret->getBody(); - } - - /** - * {@inheritDoc} - */ - public function fetchFull(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = '') - { - return $this->get( - $url, - [ - 'timeout' => $timeout, - 'accept_content' => $accept_content, - 'cookiejar' => $cookiejar - ] - ); - } -} diff --git a/src/Network/HTTPClient/Capability/ICanHandleHttpResponses.php b/src/Network/HTTPClient/Capability/ICanHandleHttpResponses.php new file mode 100644 index 0000000000..b0ad0a9682 --- /dev/null +++ b/src/Network/HTTPClient/Capability/ICanHandleHttpResponses.php @@ -0,0 +1,105 @@ +. + * + */ + +namespace Friendica\Network\HTTPClient\Capability; + +use GuzzleHttp\Exception\TransferException; + +/** + * Interface for calling HTTP requests and returning their responses + */ +interface ICanRequestPerHttp +{ + /** + * Fetches the content of an URL + * + * Set the cookiejar argument to a string (e.g. "/tmp/friendica-cookies.txt") + * to preserve cookies from one request to the next. + * + * @param string $url URL to fetch + * @param int $timeout Timeout in seconds, default system config value or 60 seconds + * @param string $accept_content supply Accept: header with 'accept_content' as the value + * @param string $cookiejar Path to cookie jar file + * + * @return string The fetched content + */ + public function fetch(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = ''): string; + + /** + * Fetches the whole response of an URL. + * + * Inner workings and parameters are the same as @ref fetchUrl but returns an array with + * all the information collected during the fetch. + * + * @param string $url URL to fetch + * @param int $timeout Timeout in seconds, default system config value or 60 seconds + * @param string $accept_content supply Accept: header with 'accept_content' as the value + * @param string $cookiejar Path to cookie jar file + * + * @return ICanHandleHttpResponses With all relevant information, 'body' contains the actual fetched content. + */ + public function fetchFull(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = ''): ICanHandleHttpResponses; + + /** + * Send a HEAD to a URL. + * + * @param string $url URL to fetch + * @param array $opts (optional parameters) associative array with: + * 'accept_content' => (string array) supply Accept: header with 'accept_content' as the value + * 'timeout' => int Timeout in seconds, default system config value or 60 seconds + * 'cookiejar' => path to cookie jar file + * 'header' => header array + * + * @return ICanHandleHttpResponses + */ + public function head(string $url, array $opts = []): ICanHandleHttpResponses; + + /** + * Send a GET to an URL. + * + * @param string $url URL to fetch + * @param array $opts (optional parameters) associative array with: + * 'accept_content' => (string array) supply Accept: header with 'accept_content' as the value + * 'timeout' => int Timeout in seconds, default system config value or 60 seconds + * 'cookiejar' => path to cookie jar file + * 'header' => header array + * 'content_length' => int maximum File content length + * + * @return ICanHandleHttpResponses + */ + public function get(string $url, array $opts = []): ICanHandleHttpResponses; + + /** + * Sends a HTTP request to a given url + * + * @param string $method A HTTP request + * @param string $url Url to send to + * @param array $opts (optional parameters) associative array with: + * 'body' => (mixed) setting the body for sending data + * 'accept_content' => (string array) supply Accept: header with 'accept_content' as the value + * 'timeout' => int Timeout in seconds, default system config value or 60 seconds + * 'cookiejar' => path to cookie jar file + * 'header' => header array + * 'content_length' => int maximum File content length + * 'auth' => array authentication settings + * + * @return ICanHandleHttpResponses + */ + public function request(string $method, string $url, array $opts = []): ICanHandleHttpResponses; + + /** + * Send POST request to an URL + * + * @param string $url URL to post + * @param mixed $params array of POST variables + * @param array $headers HTTP headers + * @param int $timeout The timeout in seconds, default system config value or 60 seconds + * + * @return ICanHandleHttpResponses The content + */ + public function post(string $url, $params, array $headers = [], int $timeout = 0): ICanHandleHttpResponses; + + /** + * Returns the original URL of the provided URL + * + * This function strips tracking query params and follows redirections, either + * through HTTP code or meta refresh tags. Stops after 10 redirections. + * + * @param string $url A user-submitted URL + * + * @return string A canonical URL + * + * @throws TransferException In case there's an error during the resolving + */ + public function finalUrl(string $url): string; +} diff --git a/src/Network/HTTPClient/Client/HttpClientCan.php b/src/Network/HTTPClient/Client/HttpClientCan.php new file mode 100644 index 0000000000..ea07a5c83d --- /dev/null +++ b/src/Network/HTTPClient/Client/HttpClientCan.php @@ -0,0 +1,258 @@ +. + * + */ + +namespace Friendica\Network\HTTPClient\Client; + +use Friendica\Core\System; +use Friendica\Network\HTTPClient\Response\CurlResult; +use Friendica\Network\HTTPClient\Response\GuzzleResponse; +use Friendica\Network\HTTPClient\Capability\ICanRequestPerHttp; +use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses; +use Friendica\Network\HTTPException\InternalServerErrorException; +use Friendica\Util\Network; +use Friendica\Util\Profiler; +use GuzzleHttp\Client; +use GuzzleHttp\Cookie\FileCookieJar; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Exception\TransferException; +use GuzzleHttp\RequestOptions; +use mattwright\URLResolver; +use Psr\Http\Message\ResponseInterface; +use Psr\Log\InvalidArgumentException; +use Psr\Log\LoggerInterface; + +/** + * Performs HTTP requests to a given URL + */ +class HttpClientCan implements ICanRequestPerHttp +{ + /** @var LoggerInterface */ + private $logger; + /** @var Profiler */ + private $profiler; + /** @var Client */ + private $client; + /** @var URLResolver */ + private $resolver; + + public function __construct(LoggerInterface $logger, Profiler $profiler, Client $client, URLResolver $resolver) + { + $this->logger = $logger; + $this->profiler = $profiler; + $this->client = $client; + $this->resolver = $resolver; + } + + /** + * {@inheritDoc} + */ + public function request(string $method, string $url, array $opts = []): ICanHandleHttpResponses + { + $this->profiler->startRecording('network'); + $this->logger->debug('Request start.', ['url' => $url, 'method' => $method]); + + if (Network::isLocalLink($url)) { + $this->logger->info('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); + } + + if (strlen($url) > 1000) { + $this->logger->debug('URL is longer than 1000 characters.', ['url' => $url, 'callstack' => System::callstack(20)]); + $this->profiler->stopRecording(); + return CurlResult::createErrorCurl(substr($url, 0, 200)); + } + + $parts2 = []; + $parts = parse_url($url); + $path_parts = explode('/', $parts['path'] ?? ''); + foreach ($path_parts as $part) { + if (strlen($part) <> mb_strlen($part)) { + $parts2[] = rawurlencode($part); + } else { + $parts2[] = $part; + } + } + $parts['path'] = implode('/', $parts2); + $url = Network::unparseURL($parts); + + if (Network::isUrlBlocked($url)) { + $this->logger->info('Domain is blocked.', ['url' => $url]); + $this->profiler->stopRecording(); + return CurlResult::createErrorCurl($url); + } + + $conf = []; + + if (!empty($opts[HttpClientOptions::COOKIEJAR])) { + $jar = new FileCookieJar($opts[HttpClientOptions::COOKIEJAR]); + $conf[RequestOptions::COOKIES] = $jar; + } + + $headers = []; + + if (!empty($opts[HttpClientOptions::ACCEPT_CONTENT])) { + $headers['Accept'] = $opts[HttpClientOptions::ACCEPT_CONTENT]; + } + + if (!empty($opts[HttpClientOptions::LEGACY_HEADER])) { + $this->logger->notice('Wrong option \'headers\' used.'); + $headers = array_merge($opts[HttpClientOptions::LEGACY_HEADER], $headers); + } + + if (!empty($opts[HttpClientOptions::HEADERS])) { + $headers = array_merge($opts[HttpClientOptions::HEADERS], $headers); + } + + $conf[RequestOptions::HEADERS] = array_merge($this->client->getConfig(RequestOptions::HEADERS), $headers); + + if (!empty($opts[HttpClientOptions::TIMEOUT])) { + $conf[RequestOptions::TIMEOUT] = $opts[HttpClientOptions::TIMEOUT]; + } + + if (!empty($opts[HttpClientOptions::BODY])) { + $conf[RequestOptions::BODY] = $opts[HttpClientOptions::BODY]; + } + + if (!empty($opts[HttpClientOptions::AUTH])) { + $conf[RequestOptions::AUTH] = $opts[HttpClientOptions::AUTH]; + } + + $conf[RequestOptions::ON_HEADERS] = function (ResponseInterface $response) use ($opts) { + if (!empty($opts[HttpClientOptions::CONTENT_LENGTH]) && + (int)$response->getHeaderLine('Content-Length') > $opts[HttpClientOptions::CONTENT_LENGTH]) { + throw new TransferException('The file is too big!'); + } + }; + + try { + $this->logger->debug('http request config.', ['url' => $url, 'method' => $method, 'options' => $conf]); + + $response = $this->client->request($method, $url, $conf); + return new GuzzleResponse($response, $url); + } catch (TransferException $exception) { + if ($exception instanceof RequestException && + $exception->hasResponse()) { + return new GuzzleResponse($exception->getResponse(), $url, $exception->getCode(), ''); + } else { + return new CurlResult($url, '', ['http_code' => 500], $exception->getCode(), ''); + } + } catch (InvalidArgumentException | \InvalidArgumentException $argumentException) { + $this->logger->info('Invalid Argument for HTTP call.', ['url' => $url, 'method' => $method, 'exception' => $argumentException]); + return new CurlResult($url, '', ['http_code' => 500], $argumentException->getCode(), $argumentException->getMessage()); + } finally { + $this->logger->debug('Request stop.', ['url' => $url, 'method' => $method]); + $this->profiler->stopRecording(); + } + } + + /** {@inheritDoc} + */ + public function head(string $url, array $opts = []): ICanHandleHttpResponses + { + return $this->request('head', $url, $opts); + } + + /** + * {@inheritDoc} + */ + public function get(string $url, array $opts = []): ICanHandleHttpResponses + { + return $this->request('get', $url, $opts); + } + + /** + * {@inheritDoc} + */ + public function post(string $url, $params, array $headers = [], int $timeout = 0): ICanHandleHttpResponses + { + $opts = []; + + $opts[HttpClientOptions::BODY] = $params; + + if (!empty($headers)) { + $opts[HttpClientOptions::HEADERS] = $headers; + } + + if (!empty($timeout)) { + $opts[HttpClientOptions::TIMEOUT] = $timeout; + } + + return $this->request('post', $url, $opts); + } + + /** + * {@inheritDoc} + */ + public function finalUrl(string $url): string + { + $this->profiler->startRecording('network'); + + if (Network::isLocalLink($url)) { + $this->logger->debug('Local link', ['url' => $url, 'callstack' => System::callstack(20)]); + } + + if (Network::isUrlBlocked($url)) { + $this->logger->info('Domain is blocked.', ['url' => $url]); + return $url; + } + + if (Network::isRedirectBlocked($url)) { + $this->logger->info('Domain should not be redirected.', ['url' => $url]); + return $url; + } + + $url = Network::stripTrackingQueryParams($url); + + $url = trim($url, "'"); + + $urlResult = $this->resolver->resolveURL($url); + + if ($urlResult->didErrorOccur()) { + throw new TransferException($urlResult->getErrorMessageString(), $urlResult->getHTTPStatusCode()); + } + + return $urlResult->getURL(); + } + + /** + * {@inheritDoc} + */ + public function fetch(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = ''): string + { + $ret = $this->fetchFull($url, $timeout, $accept_content, $cookiejar); + + return $ret->getBody(); + } + + /** + * {@inheritDoc} + */ + public function fetchFull(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = ''): ICanHandleHttpResponses + { + return $this->get( + $url, + [ + 'timeout' => $timeout, + 'accept_content' => $accept_content, + 'cookiejar' => $cookiejar + ] + ); + } +} diff --git a/src/Network/HTTPClient/Client/HttpClientOptions.php b/src/Network/HTTPClient/Client/HttpClientOptions.php new file mode 100644 index 0000000000..fa684c7631 --- /dev/null +++ b/src/Network/HTTPClient/Client/HttpClientOptions.php @@ -0,0 +1,44 @@ +config = $config; + $this->profiler = $profiler; + $this->baseUrl = $baseUrl; + } + + /** + * Creates a IHTTPClient for communications with HTTP endpoints + * + * @param HandlerStack|null $handlerStack (optional) A handler replacement (just usefull at test environments) + * + * @return ICanRequestPerHttp + */ + public function createClient(HandlerStack $handlerStack = null): ICanRequestPerHttp + { + $proxy = $this->config->get('system', 'proxy'); + + if (!empty($proxy)) { + $proxyUser = $this->config->get('system', 'proxyuser'); + + if (!empty($proxyUser)) { + $proxy = $proxyUser . '@' . $proxy; + } + } + + $logger = $this->logger; + + $onRedirect = function ( + RequestInterface $request, + ResponseInterface $response, + UriInterface $uri + ) use ($logger) { + $logger->notice('Curl redirect.', ['url' => $request->getUri(), 'to' => $uri, 'method' => $request->getMethod()]); + }; + + $userAgent = FRIENDICA_PLATFORM . " '" . + FRIENDICA_CODENAME . "' " . + FRIENDICA_VERSION . '-' . + DB_UPDATE_VERSION . '; ' . + $this->baseUrl->get(); + + $guzzle = new GuzzleHttp\Client([ + RequestOptions::ALLOW_REDIRECTS => [ + 'max' => 8, + 'on_redirect' => $onRedirect, + 'track_redirect' => true, + 'strict' => true, + 'referer' => true, + ], + RequestOptions::HTTP_ERRORS => false, + // Without this setting it seems as if some webservers send compressed content + // This seems to confuse curl so that it shows this uncompressed. + /// @todo We could possibly set this value to "gzip" or something similar + RequestOptions::DECODE_CONTENT => '', + RequestOptions::FORCE_IP_RESOLVE => ($this->config->get('system', 'ipv4_resolve') ? 'v4' : null), + RequestOptions::CONNECT_TIMEOUT => 10, + RequestOptions::TIMEOUT => $this->config->get('system', 'curl_timeout', 60), + // by default, we will allow self-signed certs, + // but it can be overridden + RequestOptions::VERIFY => (bool)$this->config->get('system', 'verifyssl'), + RequestOptions::PROXY => $proxy, + RequestOptions::HEADERS => [ + 'User-Agent' => $userAgent, + ], + 'handler' => $handlerStack ?? HandlerStack::create(), + ]); + + $resolver = new URLResolver(); + $resolver->setUserAgent($userAgent); + $resolver->setMaxRedirects(10); + $resolver->setRequestTimeout(10); + // if the file is too large then exit + $resolver->setMaxResponseDataSize(1000000); + // Designate a temporary file that will store cookies during the session. + // Some websites test the browser for cookie support, so this enhances results. + $resolver->setCookieJar(get_temppath() .'/resolver-cookie-' . Strings::getRandomName(10)); + + return new Client\HttpClientCan($logger, $this->profiler, $guzzle, $resolver); + } +} diff --git a/src/Network/HTTPClient/Response/CurlResult.php b/src/Network/HTTPClient/Response/CurlResult.php new file mode 100644 index 0000000000..adff9b8dca --- /dev/null +++ b/src/Network/HTTPClient/Response/CurlResult.php @@ -0,0 +1,349 @@ +. + * + */ + +namespace Friendica\Network\HTTPClient\Response; + +use Friendica\Core\Logger; +use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses; +use Friendica\Network\HTTPException\InternalServerErrorException; +use Friendica\Util\Network; + +/** + * A content class for Curl call results + */ +class CurlResult implements ICanHandleHttpResponses +{ + /** + * @var int HTTP return code or 0 if timeout or failure + */ + private $returnCode; + + /** + * @var string the content type of the Curl call + */ + private $contentType; + + /** + * @var string the HTTP headers of the Curl call + */ + private $header; + + /** + * @var array the HTTP headers of the Curl call + */ + private $header_fields; + + /** + * @var boolean true (if HTTP 2xx result) or false + */ + private $isSuccess; + + /** + * @var string the URL which was called + */ + private $url; + + /** + * @var string in case of redirect, content was finally retrieved from this URL + */ + private $redirectUrl; + + /** + * @var string fetched content + */ + private $body; + + /** + * @var array some informations about the fetched data + */ + private $info; + + /** + * @var boolean true if the URL has a redirect + */ + private $isRedirectUrl; + + /** + * @var boolean true if the curl request timed out + */ + private $isTimeout; + + /** + * @var int the error number or 0 (zero) if no error + */ + private $errorNumber; + + /** + * @var string the error message or '' (the empty string) if no + */ + private $error; + + /** + * Creates an errored CURL response + * + * @param string $url optional URL + * + * @return ICanHandleHttpResponses a CURL with error response + * @throws InternalServerErrorException + */ + public static function createErrorCurl(string $url = '') + { + return new CurlResult($url, '', ['http_code' => 0]); + } + + /** + * Curl constructor. + * + * @param string $url the URL which was called + * @param string $result the result of the curl execution + * @param array $info an additional info array + * @param int $errorNumber the error number or 0 (zero) if no error + * @param string $error the error message or '' (the empty string) if no + * + * @throws InternalServerErrorException when HTTP code of the CURL response is missing + */ + public function __construct(string $url, string $result, array $info, int $errorNumber = 0, string $error = '') + { + if (!array_key_exists('http_code', $info)) { + throw new InternalServerErrorException('CURL response doesn\'t contains a response HTTP code'); + } + + $this->returnCode = $info['http_code']; + $this->url = $url; + $this->info = $info; + $this->errorNumber = $errorNumber; + $this->error = $error; + + Logger::debug('construct', ['url' => $url, 'returncode' => $this->returnCode, 'result' => $result]); + + $this->parseBodyHeader($result); + $this->checkSuccess(); + $this->checkRedirect(); + $this->checkInfo(); + } + + private function parseBodyHeader($result) + { + // Pull out multiple headers, e.g. proxy and continuation headers + // allow for HTTP/2.x without fixing code + + $header = ''; + $base = $result; + while (preg_match('/^HTTP\/.+? \d+/', $base)) { + $chunk = substr($base, 0, strpos($base, "\r\n\r\n") + 4); + $header .= $chunk; + $base = substr($base, strlen($chunk)); + } + + $this->body = substr($result, strlen($header)); + $this->header = $header; + $this->header_fields = []; // Is filled on demand + } + + private function checkSuccess() + { + $this->isSuccess = ($this->returnCode >= 200 && $this->returnCode <= 299) || $this->errorNumber == 0; + + // Everything higher or equal 400 is not a success + if ($this->returnCode >= 400) { + $this->isSuccess = false; + } + + if (!$this->isSuccess) { + Logger::debug('debug', ['info' => $this->info]); + } + + if (!$this->isSuccess && $this->errorNumber == CURLE_OPERATION_TIMEDOUT) { + $this->isTimeout = true; + } else { + $this->isTimeout = false; + } + } + + private function checkRedirect() + { + if (!array_key_exists('url', $this->info)) { + $this->redirectUrl = ''; + } else { + $this->redirectUrl = $this->info['url']; + } + + if ($this->returnCode == 301 || $this->returnCode == 302 || $this->returnCode == 303 || $this->returnCode == 307) { + $redirect_parts = parse_url($this->info['redirect_url'] ?? ''); + if (empty($redirect_parts)) { + $redirect_parts = []; + } + + if (preg_match('/(Location:|URI:)(.*?)\n/i', $this->header, $matches)) { + $redirect_parts2 = parse_url(trim(array_pop($matches))); + if (!empty($redirect_parts2)) { + $redirect_parts = array_merge($redirect_parts, $redirect_parts2); + } + } + + $parts = parse_url($this->info['url'] ?? ''); + if (empty($parts)) { + $parts = []; + } + + /// @todo Checking the corresponding RFC which parts of a redirect can be ommitted. + $components = ['scheme', 'host', 'path', 'query', 'fragment']; + foreach ($components as $component) { + if (empty($redirect_parts[$component]) && !empty($parts[$component])) { + $redirect_parts[$component] = $parts[$component]; + } + } + + $this->redirectUrl = Network::unparseURL($redirect_parts); + + $this->isRedirectUrl = true; + } else { + $this->isRedirectUrl = false; + } + } + + private function checkInfo() + { + if (isset($this->info['content_type'])) { + $this->contentType = $this->info['content_type']; + } else { + $this->contentType = ''; + } + } + + /** {@inheritDoc} */ + public function getReturnCode(): string + { + return $this->returnCode; + } + + /** {@inheritDoc} */ + public function getContentType(): string + { + return $this->contentType; + } + + /** {@inheritDoc} */ + public function getHeader(string $header): array + { + if (empty($header)) { + return []; + } + + $header = strtolower(trim($header)); + + $headers = $this->getHeaderArray(); + + if (isset($headers[$header])) { + return $headers[$header]; + } + + return []; + } + + /** {@inheritDoc} */ + public function getHeaders(): array + { + return $this->getHeaderArray(); + } + + /** {@inheritDoc} */ + public function inHeader(string $field): bool + { + $field = strtolower(trim($field)); + + $headers = $this->getHeaderArray(); + + return array_key_exists($field, $headers); + } + + /** {@inheritDoc} */ + public function getHeaderArray(): array + { + if (!empty($this->header_fields)) { + return $this->header_fields; + } + + $this->header_fields = []; + + $lines = explode("\n", trim($this->header)); + foreach ($lines as $line) { + $parts = explode(':', $line); + $headerfield = strtolower(trim(array_shift($parts))); + $headerdata = trim(implode(':', $parts)); + if (empty($this->header_fields[$headerfield])) { + $this->header_fields[$headerfield] = [$headerdata]; + } elseif (!in_array($headerdata, $this->header_fields[$headerfield])) { + $this->header_fields[$headerfield][] = $headerdata; + } + } + + return $this->header_fields; + } + + /** {@inheritDoc} */ + public function isSuccess(): bool + { + return $this->isSuccess; + } + + /** {@inheritDoc} */ + public function getUrl(): string + { + return $this->url; + } + + /** {@inheritDoc} */ + public function getRedirectUrl(): string + { + return $this->redirectUrl; + } + + /** {@inheritDoc} */ + public function getBody(): string + { + return $this->body; + } + + /** {@inheritDoc} */ + public function isRedirectUrl(): bool + { + return $this->isRedirectUrl; + } + + /** {@inheritDoc} */ + public function getErrorNumber(): int + { + return $this->errorNumber; + } + + /** {@inheritDoc} */ + public function getError(): string + { + return $this->error; + } + + /** {@inheritDoc} */ + public function isTimeout(): bool + { + return $this->isTimeout; + } +} diff --git a/src/Network/HTTPClient/Response/GuzzleResponse.php b/src/Network/HTTPClient/Response/GuzzleResponse.php new file mode 100644 index 0000000000..aa92309e5d --- /dev/null +++ b/src/Network/HTTPClient/Response/GuzzleResponse.php @@ -0,0 +1,157 @@ +. + * + */ + +namespace Friendica\Network\HTTPClient\Response; + +use Friendica\Core\Logger; +use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses; +use Friendica\Network\HTTPException\NotImplementedException; +use GuzzleHttp\Psr7\Response; +use Psr\Http\Message\ResponseInterface; + +/** + * A content wrapper class for Guzzle call results + */ +class GuzzleResponse extends Response implements ICanHandleHttpResponses, ResponseInterface +{ + /** @var string The URL */ + private $url; + /** @var boolean */ + private $isTimeout; + /** @var boolean */ + private $isSuccess; + /** + * @var int the error number or 0 (zero) if no error + */ + private $errorNumber; + + /** + * @var string the error message or '' (the empty string) if no + */ + private $error; + + public function __construct(ResponseInterface $response, string $url, $errorNumber = 0, $error = '') + { + parent::__construct($response->getStatusCode(), $response->getHeaders(), $response->getBody(), $response->getProtocolVersion(), $response->getReasonPhrase()); + $this->url = $url; + $this->error = $error; + $this->errorNumber = $errorNumber; + + $this->checkSuccess(); + } + + private function checkSuccess() + { + $this->isSuccess = ($this->getStatusCode() >= 200 && $this->getStatusCode() <= 299) || $this->errorNumber == 0; + + // Everything higher or equal 400 is not a success + if ($this->getReturnCode() >= 400) { + $this->isSuccess = false; + } + + if (!$this->isSuccess) { + Logger::debug('debug', ['info' => $this->getHeaders()]); + } + + if (!$this->isSuccess && $this->errorNumber == CURLE_OPERATION_TIMEDOUT) { + $this->isTimeout = true; + } else { + $this->isTimeout = false; + } + } + + /** {@inheritDoc} */ + public function getReturnCode(): string + { + return $this->getStatusCode(); + } + + /** {@inheritDoc} */ + public function getContentType(): string + { + $contentTypes = $this->getHeader('Content-Type') ?? []; + + return array_pop($contentTypes) ?? ''; + } + + /** {@inheritDoc} */ + public function inHeader(string $field): bool + { + return $this->hasHeader($field); + } + + /** {@inheritDoc} */ + public function getHeaderArray(): array + { + return $this->getHeaders(); + } + + /** {@inheritDoc} */ + public function isSuccess(): bool + { + return $this->isSuccess; + } + + /** {@inheritDoc} */ + public function getUrl(): string + { + return $this->url; + } + + /** {@inheritDoc} */ + public function getRedirectUrl(): string + { + return $this->url; + } + + /** {@inheritDoc} + * + * @throws NotImplementedException + */ + public function isRedirectUrl(): bool + { + throw new NotImplementedException(); + } + + /** {@inheritDoc} */ + public function getErrorNumber(): int + { + return $this->errorNumber; + } + + /** {@inheritDoc} */ + public function getError(): string + { + return $this->error; + } + + /** {@inheritDoc} */ + public function isTimeout(): bool + { + return $this->isTimeout; + } + + /// @todo - fix mismatching use of "getBody()" as string here and parent "getBody()" as streaminterface + public function getBody(): string + { + return (string) parent::getBody(); + } +} diff --git a/src/Network/HTTPClientOptions.php b/src/Network/HTTPClientOptions.php deleted file mode 100644 index f9438fb479..0000000000 --- a/src/Network/HTTPClientOptions.php +++ /dev/null @@ -1,44 +0,0 @@ -. - * - */ - -namespace Friendica\Network; - -use GuzzleHttp\Exception\TransferException; - -/** - * Interface for calling HTTP requests and returning their responses - */ -interface IHTTPClient -{ - /** - * Fetches the content of an URL - * - * Set the cookiejar argument to a string (e.g. "/tmp/friendica-cookies.txt") - * to preserve cookies from one request to the next. - * - * @param string $url URL to fetch - * @param int $timeout Timeout in seconds, default system config value or 60 seconds - * @param string $accept_content supply Accept: header with 'accept_content' as the value - * @param string $cookiejar Path to cookie jar file - * - * @return string The fetched content - */ - public function fetch(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = ''); - - /** - * Fetches the whole response of an URL. - * - * Inner workings and parameters are the same as @ref fetchUrl but returns an array with - * all the information collected during the fetch. - * - * @param string $url URL to fetch - * @param int $timeout Timeout in seconds, default system config value or 60 seconds - * @param string $accept_content supply Accept: header with 'accept_content' as the value - * @param string $cookiejar Path to cookie jar file - * - * @return IHTTPResult With all relevant information, 'body' contains the actual fetched content. - */ - public function fetchFull(string $url, int $timeout = 0, string $accept_content = '', string $cookiejar = ''); - - /** - * Send a HEAD to an URL. - * - * @param string $url URL to fetch - * @param array $opts (optional parameters) associative array with: - * 'accept_content' => (string array) supply Accept: header with 'accept_content' as the value - * 'timeout' => int Timeout in seconds, default system config value or 60 seconds - * 'cookiejar' => path to cookie jar file - * 'header' => header array - * - * @return CurlResult - */ - public function head(string $url, array $opts = []); - - /** - * Send a GET to an URL. - * - * @param string $url URL to fetch - * @param array $opts (optional parameters) associative array with: - * 'accept_content' => (string array) supply Accept: header with 'accept_content' as the value - * 'timeout' => int Timeout in seconds, default system config value or 60 seconds - * 'cookiejar' => path to cookie jar file - * 'header' => header array - * 'content_length' => int maximum File content length - * - * @return IHTTPResult - */ - public function get(string $url, array $opts = []); - - /** - * Sends a HTTP request to a given url - * - * @param string $method A HTTP request - * @param string $url Url to send to - * @param array $opts (optional parameters) associative array with: - * 'body' => (mixed) setting the body for sending data - * 'accept_content' => (string array) supply Accept: header with 'accept_content' as the value - * 'timeout' => int Timeout in seconds, default system config value or 60 seconds - * 'cookiejar' => path to cookie jar file - * 'header' => header array - * 'content_length' => int maximum File content length - * 'auth' => array authentication settings - * - * @return IHTTPResult - */ - public function request(string $method, string $url, array $opts = []); - - /** - * Send POST request to an URL - * - * @param string $url URL to post - * @param mixed $params array of POST variables - * @param array $headers HTTP headers - * @param int $timeout The timeout in seconds, default system config value or 60 seconds - * - * @return IHTTPResult The content - */ - public function post(string $url, $params, array $headers = [], int $timeout = 0); - - /** - * Returns the original URL of the provided URL - * - * This function strips tracking query params and follows redirections, either - * through HTTP code or meta refresh tags. Stops after 10 redirections. - * - * @param string $url A user-submitted URL - * - * @return string A canonical URL - * - * @throws TransferException In case there's an error during the resolving - */ - public function finalUrl(string $url); -} diff --git a/src/Network/IHTTPResult.php b/src/Network/IHTTPResult.php deleted file mode 100644 index 38a1176284..0000000000 --- a/src/Network/IHTTPResult.php +++ /dev/null @@ -1,104 +0,0 @@ - $host, 'ssl_url' => $ssl_url, 'url' => $url, 'callstack' => System::callstack(20)]); $xrd = null; - $curlResult = DI::httpClient()->get($ssl_url, [HTTPClientOptions::TIMEOUT => $xrd_timeout, HTTPClientOptions::ACCEPT_CONTENT => ['application/xrd+xml']]); + $curlResult = DI::httpClient()->get($ssl_url, [HttpClientOptions::TIMEOUT => $xrd_timeout, HttpClientOptions::ACCEPT_CONTENT => ['application/xrd+xml']]); $ssl_connection_error = ($curlResult->getErrorNumber() == CURLE_COULDNT_CONNECT) || ($curlResult->getReturnCode() == 0); if ($curlResult->isSuccess()) { $xml = $curlResult->getBody(); @@ -187,7 +188,7 @@ class Probe } if (!is_object($xrd) && !empty($url)) { - $curlResult = DI::httpClient()->get($url, [HTTPClientOptions::TIMEOUT => $xrd_timeout, HTTPClientOptions::ACCEPT_CONTENT => ['application/xrd+xml']]); + $curlResult = DI::httpClient()->get($url, [HttpClientOptions::TIMEOUT => $xrd_timeout, HttpClientOptions::ACCEPT_CONTENT => ['application/xrd+xml']]); $connection_error = ($curlResult->getErrorNumber() == CURLE_COULDNT_CONNECT) || ($curlResult->getReturnCode() == 0); if ($curlResult->isTimeout()) { Logger::info('Probing timeout', ['url' => $url]); @@ -429,7 +430,7 @@ class Probe */ private static function getHideStatus($url) { - $curlResult = DI::httpClient()->get($url, [HTTPClientOptions::CONTENT_LENGTH => 1000000]); + $curlResult = DI::httpClient()->get($url, [HttpClientOptions::CONTENT_LENGTH => 1000000]); if (!$curlResult->isSuccess()) { return false; } @@ -950,7 +951,7 @@ class Probe { $xrd_timeout = DI::config()->get('system', 'xrd_timeout', 20); - $curlResult = DI::httpClient()->get($url, [HTTPClientOptions::TIMEOUT => $xrd_timeout, HTTPClientOptions::ACCEPT_CONTENT => [$type]]); + $curlResult = DI::httpClient()->get($url, [HttpClientOptions::TIMEOUT => $xrd_timeout, HttpClientOptions::ACCEPT_CONTENT => [$type]]); if ($curlResult->isTimeout()) { self::$istimeout = true; return []; diff --git a/src/Protocol/OStatus.php b/src/Protocol/OStatus.php index 46f68086b1..ca8a76b6b4 100644 --- a/src/Protocol/OStatus.php +++ b/src/Protocol/OStatus.php @@ -38,7 +38,7 @@ use Friendica\Model\ItemURI; use Friendica\Model\Post; use Friendica\Model\Tag; use Friendica\Model\User; -use Friendica\Network\HTTPClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Network\Probe; use Friendica\Util\DateTimeFormat; use Friendica\Util\Images; @@ -728,7 +728,7 @@ class OStatus self::$conv_list[$conversation] = true; - $curlResult = DI::httpClient()->get($conversation, [HTTPClientOptions::ACCEPT_CONTENT => ['application/atom+xml', 'text/html']]); + $curlResult = DI::httpClient()->get($conversation, [HttpClientOptions::ACCEPT_CONTENT => ['application/atom+xml', 'text/html']]); if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { return; @@ -922,7 +922,7 @@ class OStatus } $stored = false; - $curlResult = DI::httpClient()->get($related, [HTTPClientOptions::ACCEPT_CONTENT => ['application/atom+xml', 'text/html']]); + $curlResult = DI::httpClient()->get($related, [HttpClientOptions::ACCEPT_CONTENT => ['application/atom+xml', 'text/html']]); if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { return; diff --git a/src/Util/HTTPSignature.php b/src/Util/HTTPSignature.php index f11dbcceb4..36919b5b64 100644 --- a/src/Util/HTTPSignature.php +++ b/src/Util/HTTPSignature.php @@ -28,9 +28,9 @@ use Friendica\DI; use Friendica\Model\APContact; use Friendica\Model\Contact; use Friendica\Model\User; -use Friendica\Network\CurlResult; -use Friendica\Network\HTTPClientOptions; -use Friendica\Network\IHTTPResult; +use Friendica\Network\HTTPClient\Response\CurlResult; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; +use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses; /** * Implements HTTP Signatures per draft-cavage-http-signatures-07. @@ -414,7 +414,7 @@ class HTTPSignature * 'nobody' => only return the header * 'cookiejar' => path to cookie jar file * - * @return IHTTPResult CurlResult + * @return \Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses CurlResult * @throws \Friendica\Network\HTTPException\InternalServerErrorException */ public static function fetchRaw($request, $uid = 0, $opts = ['accept_content' => ['application/activity+json', 'application/ld+json']]) @@ -450,7 +450,7 @@ class HTTPSignature } $curl_opts = $opts; - $curl_opts[HTTPClientOptions::HEADERS] = $header; + $curl_opts[HttpClientOptions::HEADERS] = $header; if (!empty($opts['nobody'])) { $curlResult = DI::httpClient()->head($request, $curl_opts); diff --git a/src/Util/ParseUrl.php b/src/Util/ParseUrl.php index 5891649525..6c3c4a0259 100644 --- a/src/Util/ParseUrl.php +++ b/src/Util/ParseUrl.php @@ -30,7 +30,7 @@ use Friendica\Database\Database; use Friendica\Database\DBA; use Friendica\DI; use Friendica\Network\HTTPException; -use Friendica\Network\HTTPClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; /** * Get information about a given URL @@ -214,7 +214,7 @@ class ParseUrl return $siteinfo; } - $curlResult = DI::httpClient()->get($url, [HTTPClientOptions::CONTENT_LENGTH => 1000000]); + $curlResult = DI::httpClient()->get($url, [HttpClientOptions::CONTENT_LENGTH => 1000000]); if (!$curlResult->isSuccess() || empty($curlResult->getBody())) { return $siteinfo; } diff --git a/src/Worker/OnePoll.php b/src/Worker/OnePoll.php index 99d5054ca3..6d05e6cdc0 100644 --- a/src/Worker/OnePoll.php +++ b/src/Worker/OnePoll.php @@ -29,7 +29,7 @@ use Friendica\Model\Contact; use Friendica\Model\Item; use Friendica\Model\Post; use Friendica\Model\User; -use Friendica\Network\HTTPClientOptions; +use Friendica\Network\HTTPClient\Client\HttpClientOptions; use Friendica\Protocol\Activity; use Friendica\Protocol\ActivityPub; use Friendica\Protocol\Email; @@ -153,7 +153,7 @@ class OnePoll } $cookiejar = tempnam(get_temppath(), 'cookiejar-onepoll-'); - $curlResult = DI::httpClient()->get($contact['poll'], [HTTPClientOptions::COOKIEJAR => $cookiejar]); + $curlResult = DI::httpClient()->get($contact['poll'], [HttpClientOptions::COOKIEJAR => $cookiejar]); unlink($cookiejar); if ($curlResult->isTimeout()) { diff --git a/static/dependencies.config.php b/static/dependencies.config.php index 042949e60e..dad374d8da 100644 --- a/static/dependencies.config.php +++ b/static/dependencies.config.php @@ -224,8 +224,8 @@ return [ ['getBackend', [], Dice::CHAIN_CALL], ], ], - Network\IHTTPClient::class => [ - 'instanceOf' => Factory\HTTPClientFactory::class, + Network\HTTPClient\Capability\ICanRequestPerHttp::class => [ + 'instanceOf' => Network\HTTPClient\Factory\HttpClient::class, 'call' => [ ['createClient', [], Dice::CHAIN_CALL], ], diff --git a/tests/DiceHttpMockHandlerTrait.php b/tests/DiceHttpMockHandlerTrait.php index 969b76b5b0..7f77d7e4fe 100644 --- a/tests/DiceHttpMockHandlerTrait.php +++ b/tests/DiceHttpMockHandlerTrait.php @@ -23,8 +23,8 @@ namespace Friendica\Test; use Dice\Dice; use Friendica\DI; -use Friendica\Factory\HTTPClientFactory; -use Friendica\Network\IHTTPClient; +use Friendica\Network\HTTPClient\Factory\HttpClient; +use Friendica\Network\HTTPClient\Capability\ICanRequestPerHttp; use GuzzleHttp\HandlerStack; /** @@ -49,8 +49,8 @@ trait DiceHttpMockHandlerTrait $dice = DI::getDice(); // addRule() clones the current instance and returns a new one, so no concurrency problems :-) - $newDice = $dice->addRule(IHTTPClient::class, [ - 'instanceOf' => HTTPClientFactory::class, + $newDice = $dice->addRule(ICanRequestPerHttp::class, [ + 'instanceOf' => HttpClient::class, 'call' => [ ['createClient', [$this->httpRequestHandler], Dice::CHAIN_CALL], ], diff --git a/tests/src/Core/InstallerTest.php b/tests/src/Core/InstallerTest.php index 4742d41d88..8db5a7d5c0 100644 --- a/tests/src/Core/InstallerTest.php +++ b/tests/src/Core/InstallerTest.php @@ -25,8 +25,8 @@ namespace Friendica\Core; use Dice\Dice; use Friendica\Core\Config\ValueObject\Cache; use Friendica\DI; -use Friendica\Network\IHTTPResult; -use Friendica\Network\IHTTPClient; +use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses; +use Friendica\Network\HTTPClient\Capability\ICanRequestPerHttp; use Friendica\Test\MockedTest; use Friendica\Test\Util\VFSTrait; use Mockery; @@ -319,7 +319,7 @@ class InstallerTest extends MockedTest $this->l10nMock->shouldReceive('t')->andReturnUsing(function ($args) { return $args; }); // Mocking the CURL Response - $IHTTPResult = Mockery::mock(IHTTPResult::class); + $IHTTPResult = Mockery::mock(ICanHandleHttpResponses::class); $IHTTPResult ->shouldReceive('getReturnCode') ->andReturn('404'); @@ -331,7 +331,7 @@ class InstallerTest extends MockedTest ->andReturn('test Error'); // Mocking the CURL Request - $networkMock = Mockery::mock(IHTTPClient::class); + $networkMock = Mockery::mock(ICanRequestPerHttp::class); $networkMock ->shouldReceive('fetchFull') ->with('https://test/install/testrewrite') @@ -342,7 +342,7 @@ class InstallerTest extends MockedTest ->andReturn($IHTTPResult); $this->dice->shouldReceive('create') - ->with(IHTTPClient::class) + ->with(ICanRequestPerHttp::class) ->andReturn($networkMock); DI::init($this->dice); @@ -366,19 +366,19 @@ class InstallerTest extends MockedTest $this->l10nMock->shouldReceive('t')->andReturnUsing(function ($args) { return $args; }); // Mocking the failed CURL Response - $IHTTPResultF = Mockery::mock(IHTTPResult::class); + $IHTTPResultF = Mockery::mock(ICanHandleHttpResponses::class); $IHTTPResultF ->shouldReceive('getReturnCode') ->andReturn('404'); // Mocking the working CURL Response - $IHTTPResultW = Mockery::mock(IHTTPResult::class); + $IHTTPResultW = Mockery::mock(ICanHandleHttpResponses::class); $IHTTPResultW ->shouldReceive('getReturnCode') ->andReturn('204'); // Mocking the CURL Request - $networkMock = Mockery::mock(IHTTPClient::class); + $networkMock = Mockery::mock(ICanRequestPerHttp::class); $networkMock ->shouldReceive('fetchFull') ->with('https://test/install/testrewrite') @@ -389,7 +389,7 @@ class InstallerTest extends MockedTest ->andReturn($IHTTPResultW); $this->dice->shouldReceive('create') - ->with(IHTTPClient::class) + ->with(ICanRequestPerHttp::class) ->andReturn($networkMock); DI::init($this->dice); diff --git a/tests/src/Core/Storage/Repository/StorageManagerTest.php b/tests/src/Core/Storage/Repository/StorageManagerTest.php index 0ba28af82b..c090c26963 100644 --- a/tests/src/Core/Storage/Repository/StorageManagerTest.php +++ b/tests/src/Core/Storage/Repository/StorageManagerTest.php @@ -40,7 +40,7 @@ use Friendica\DI; use Friendica\Core\Config\Factory\Config; use Friendica\Core\Config\Repository; use Friendica\Core\Storage\Type; -use Friendica\Network\HTTPClient; +use Friendica\Network\HTTPClient\Client\HttpClientCan; use Friendica\Test\DatabaseTest; use Friendica\Test\Util\Database\StaticDatabase; use Friendica\Test\Util\VFSTrait; @@ -61,7 +61,7 @@ class StorageManagerTest extends DatabaseTest private $logger; /** @var L10n */ private $l10n; - /** @var HTTPClient */ + /** @var HttpClientCan */ private $httpRequest; protected function setUp(): void @@ -93,7 +93,7 @@ class StorageManagerTest extends DatabaseTest $this->l10n = \Mockery::mock(L10n::class); - $this->httpRequest = \Mockery::mock(HTTPClient::class); + $this->httpRequest = \Mockery::mock(HttpClientCan::class); } protected function tearDown(): void diff --git a/tests/src/Network/CurlResultTest.php b/tests/src/Network/CurlResultTest.php deleted file mode 100644 index c28dc5f1b9..0000000000 --- a/tests/src/Network/CurlResultTest.php +++ /dev/null @@ -1,214 +0,0 @@ -. - * - */ - -namespace Friendica\Test\src\Network; - -use Dice\Dice; -use Friendica\DI; -use Friendica\Network\CurlResult; -use Mockery\MockInterface; -use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; -use Psr\Log\NullLogger; - -class CurlResultTest extends TestCase -{ - protected function setUp(): void - { - parent::setUp(); - - /** @var Dice|MockInterface $dice */ - $dice = \Mockery::mock(Dice::class)->makePartial(); - $dice = $dice->addRules(include __DIR__ . '/../../../static/dependencies.config.php'); - - $logger = new NullLogger(); - $dice->shouldReceive('create') - ->with(LoggerInterface::class) - ->andReturn($logger); - - DI::init($dice); - } - - /** - * @small - */ - public function testNormal() - { - $header = file_get_contents(__DIR__ . '/../../datasets/curl/about.head'); - $headerArray = include(__DIR__ . '/../../datasets/curl/about.head.php'); - $body = file_get_contents(__DIR__ . '/../../datasets/curl/about.body'); - - - $curlResult = new CurlResult('https://test.local', $header . $body, [ - 'http_code' => 200, - 'content_type' => 'text/html; charset=utf-8', - 'url' => 'https://test.local' - ]); - - self::assertTrue($curlResult->isSuccess()); - self::assertFalse($curlResult->isTimeout()); - self::assertFalse($curlResult->isRedirectUrl()); - self::assertSame($headerArray, $curlResult->getHeaders()); - self::assertSame($body, $curlResult->getBody()); - self::assertSame('text/html; charset=utf-8', $curlResult->getContentType()); - self::assertSame('https://test.local', $curlResult->getUrl()); - self::assertSame('https://test.local', $curlResult->getRedirectUrl()); - } - - /** - * @small - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function testRedirect() - { - $header = file_get_contents(__DIR__ . '/../../datasets/curl/about.head'); - $headerArray = include(__DIR__ . '/../../datasets/curl/about.head.php'); - $body = file_get_contents(__DIR__ . '/../../datasets/curl/about.body'); - - - $curlResult = new CurlResult('https://test.local/test/it', $header . $body, [ - 'http_code' => 301, - 'content_type' => 'text/html; charset=utf-8', - 'url' => 'https://test.local/test/it', - 'redirect_url' => 'https://test.other' - ]); - - self::assertTrue($curlResult->isSuccess()); - self::assertFalse($curlResult->isTimeout()); - self::assertTrue($curlResult->isRedirectUrl()); - self::assertSame($headerArray, $curlResult->getHeaders()); - self::assertSame($body, $curlResult->getBody()); - self::assertSame('text/html; charset=utf-8', $curlResult->getContentType()); - self::assertSame('https://test.local/test/it', $curlResult->getUrl()); - self::assertSame('https://test.other/test/it', $curlResult->getRedirectUrl()); - } - - /** - * @small - */ - public function testTimeout() - { - $header = file_get_contents(__DIR__ . '/../../datasets/curl/about.head'); - $headerArray = include(__DIR__ . '/../../datasets/curl/about.head.php'); - $body = file_get_contents(__DIR__ . '/../../datasets/curl/about.body'); - - - $curlResult = new CurlResult('https://test.local/test/it', $header . $body, [ - 'http_code' => 500, - 'content_type' => 'text/html; charset=utf-8', - 'url' => 'https://test.local/test/it', - 'redirect_url' => 'https://test.other' - ], CURLE_OPERATION_TIMEDOUT, 'Tested error'); - - self::assertFalse($curlResult->isSuccess()); - self::assertTrue($curlResult->isTimeout()); - self::assertFalse($curlResult->isRedirectUrl()); - self::assertSame($headerArray, $curlResult->getHeaders()); - self::assertSame($body, $curlResult->getBody()); - self::assertSame('text/html; charset=utf-8', $curlResult->getContentType()); - self::assertSame('https://test.local/test/it', $curlResult->getRedirectUrl()); - self::assertSame('Tested error', $curlResult->getError()); - } - - /** - * @small - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function testRedirectHeader() - { - $header = file_get_contents(__DIR__ . '/../../datasets/curl/about.redirect'); - $headerArray = include(__DIR__ . '/../../datasets/curl/about.redirect.php'); - $body = file_get_contents(__DIR__ . '/../../datasets/curl/about.body'); - - - $curlResult = new CurlResult('https://test.local/test/it?key=value', $header . $body, [ - 'http_code' => 301, - 'content_type' => 'text/html; charset=utf-8', - 'url' => 'https://test.local/test/it?key=value', - ]); - - self::assertTrue($curlResult->isSuccess()); - self::assertFalse($curlResult->isTimeout()); - self::assertTrue($curlResult->isRedirectUrl()); - self::assertSame($headerArray, $curlResult->getHeaders()); - self::assertSame($body, $curlResult->getBody()); - self::assertSame('text/html; charset=utf-8', $curlResult->getContentType()); - self::assertSame('https://test.local/test/it?key=value', $curlResult->getUrl()); - self::assertSame('https://test.other/some/?key=value', $curlResult->getRedirectUrl()); - } - - /** - * @small - */ - public function testInHeader() - { - $header = file_get_contents(__DIR__ . '/../../datasets/curl/about.head'); - $body = file_get_contents(__DIR__ . '/../../datasets/curl/about.body'); - - $curlResult = new CurlResult('https://test.local', $header . $body, [ - 'http_code' => 200, - 'content_type' => 'text/html; charset=utf-8', - 'url' => 'https://test.local' - ]); - self::assertTrue($curlResult->inHeader('vary')); - self::assertFalse($curlResult->inHeader('wrongHeader')); - } - - /** - * @small - */ - public function testGetHeaderArray() - { - $header = file_get_contents(__DIR__ . '/../../datasets/curl/about.head'); - $body = file_get_contents(__DIR__ . '/../../datasets/curl/about.body'); - - $curlResult = new CurlResult('https://test.local', $header . $body, [ - 'http_code' => 200, - 'content_type' => 'text/html; charset=utf-8', - 'url' => 'https://test.local' - ]); - - $headers = $curlResult->getHeaderArray(); - - self::assertNotEmpty($headers); - self::assertArrayHasKey('vary', $headers); - } - - /** - * @small - */ - public function testGetHeaderWithParam() - { - $header = file_get_contents(__DIR__ . '/../../datasets/curl/about.head'); - $body = file_get_contents(__DIR__ . '/../../datasets/curl/about.body'); - - $curlResult = new CurlResult('https://test.local', $header . $body, [ - 'http_code' => 200, - 'content_type' => 'text/html; charset=utf-8', - 'url' => 'https://test.local' - ]); - - self::assertNotEmpty($curlResult->getHeaders()); - self::assertEmpty($curlResult->getHeader('wrongHeader')); - } -} diff --git a/tests/src/Network/HTTPClient/Client/HTTPClientTest.php b/tests/src/Network/HTTPClient/Client/HTTPClientTest.php new file mode 100644 index 0000000000..0e2c0b3e3b --- /dev/null +++ b/tests/src/Network/HTTPClient/Client/HTTPClientTest.php @@ -0,0 +1,33 @@ +setupHttpMockHandler(); + } + + /** + * Test for issue https://github.com/friendica/friendica/issues/10473#issuecomment-907749093 + */ + public function testInvalidURI() + { + $this->httpRequestHandler->setHandler(new MockHandler([ + new Response(301, ['Location' => 'https:///']), + ])); + + self::assertFalse(DI::httpClient()->get('https://friendica.local')->isSuccess()); + } +} diff --git a/tests/src/Network/HTTPClient/Response/CurlResultTest.php b/tests/src/Network/HTTPClient/Response/CurlResultTest.php new file mode 100644 index 0000000000..5cc27a0fd0 --- /dev/null +++ b/tests/src/Network/HTTPClient/Response/CurlResultTest.php @@ -0,0 +1,214 @@ +. + * + */ + +namespace Friendica\Test\src\Network\HTTPClient\Response; + +use Dice\Dice; +use Friendica\DI; +use Friendica\Network\HTTPClient\Response\CurlResult; +use Mockery\MockInterface; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + +class CurlResultTest extends TestCase +{ + protected function setUp(): void + { + parent::setUp(); + + /** @var Dice|MockInterface $dice */ + $dice = \Mockery::mock(Dice::class)->makePartial(); + $dice = $dice->addRules(include __DIR__ . '/../../../../../static/dependencies.config.php'); + + $logger = new NullLogger(); + $dice->shouldReceive('create') + ->with(LoggerInterface::class) + ->andReturn($logger); + + DI::init($dice); + } + + /** + * @small + */ + public function testNormal() + { + $header = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.head'); + $headerArray = include(__DIR__ . '/../../../../datasets/curl/about.head.php'); + $body = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.body'); + + + $curlResult = new \Friendica\Network\HTTPClient\Response\CurlResult('https://test.local', $header . $body, [ + 'http_code' => 200, + 'content_type' => 'text/html; charset=utf-8', + 'url' => 'https://test.local' + ]); + + self::assertTrue($curlResult->isSuccess()); + self::assertFalse($curlResult->isTimeout()); + self::assertFalse($curlResult->isRedirectUrl()); + self::assertSame($headerArray, $curlResult->getHeaders()); + self::assertSame($body, $curlResult->getBody()); + self::assertSame('text/html; charset=utf-8', $curlResult->getContentType()); + self::assertSame('https://test.local', $curlResult->getUrl()); + self::assertSame('https://test.local', $curlResult->getRedirectUrl()); + } + + /** + * @small + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testRedirect() + { + $header = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.head'); + $headerArray = include(__DIR__ . '/../../../../datasets/curl/about.head.php'); + $body = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.body'); + + + $curlResult = new \Friendica\Network\HTTPClient\Response\CurlResult('https://test.local/test/it', $header . $body, [ + 'http_code' => 301, + 'content_type' => 'text/html; charset=utf-8', + 'url' => 'https://test.local/test/it', + 'redirect_url' => 'https://test.other' + ]); + + self::assertTrue($curlResult->isSuccess()); + self::assertFalse($curlResult->isTimeout()); + self::assertTrue($curlResult->isRedirectUrl()); + self::assertSame($headerArray, $curlResult->getHeaders()); + self::assertSame($body, $curlResult->getBody()); + self::assertSame('text/html; charset=utf-8', $curlResult->getContentType()); + self::assertSame('https://test.local/test/it', $curlResult->getUrl()); + self::assertSame('https://test.other/test/it', $curlResult->getRedirectUrl()); + } + + /** + * @small + */ + public function testTimeout() + { + $header = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.head'); + $headerArray = include(__DIR__ . '/../../../../datasets/curl/about.head.php'); + $body = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.body'); + + + $curlResult = new \Friendica\Network\HTTPClient\Response\CurlResult('https://test.local/test/it', $header . $body, [ + 'http_code' => 500, + 'content_type' => 'text/html; charset=utf-8', + 'url' => 'https://test.local/test/it', + 'redirect_url' => 'https://test.other' + ], CURLE_OPERATION_TIMEDOUT, 'Tested error'); + + self::assertFalse($curlResult->isSuccess()); + self::assertTrue($curlResult->isTimeout()); + self::assertFalse($curlResult->isRedirectUrl()); + self::assertSame($headerArray, $curlResult->getHeaders()); + self::assertSame($body, $curlResult->getBody()); + self::assertSame('text/html; charset=utf-8', $curlResult->getContentType()); + self::assertSame('https://test.local/test/it', $curlResult->getRedirectUrl()); + self::assertSame('Tested error', $curlResult->getError()); + } + + /** + * @small + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function testRedirectHeader() + { + $header = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.redirect'); + $headerArray = include(__DIR__ . '/../../../../datasets/curl/about.redirect.php'); + $body = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.body'); + + + $curlResult = new CurlResult('https://test.local/test/it?key=value', $header . $body, [ + 'http_code' => 301, + 'content_type' => 'text/html; charset=utf-8', + 'url' => 'https://test.local/test/it?key=value', + ]); + + self::assertTrue($curlResult->isSuccess()); + self::assertFalse($curlResult->isTimeout()); + self::assertTrue($curlResult->isRedirectUrl()); + self::assertSame($headerArray, $curlResult->getHeaders()); + self::assertSame($body, $curlResult->getBody()); + self::assertSame('text/html; charset=utf-8', $curlResult->getContentType()); + self::assertSame('https://test.local/test/it?key=value', $curlResult->getUrl()); + self::assertSame('https://test.other/some/?key=value', $curlResult->getRedirectUrl()); + } + + /** + * @small + */ + public function testInHeader() + { + $header = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.head'); + $body = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.body'); + + $curlResult = new \Friendica\Network\HTTPClient\Response\CurlResult('https://test.local', $header . $body, [ + 'http_code' => 200, + 'content_type' => 'text/html; charset=utf-8', + 'url' => 'https://test.local' + ]); + self::assertTrue($curlResult->inHeader('vary')); + self::assertFalse($curlResult->inHeader('wrongHeader')); + } + + /** + * @small + */ + public function testGetHeaderArray() + { + $header = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.head'); + $body = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.body'); + + $curlResult = new \Friendica\Network\HTTPClient\Response\CurlResult('https://test.local', $header . $body, [ + 'http_code' => 200, + 'content_type' => 'text/html; charset=utf-8', + 'url' => 'https://test.local' + ]); + + $headers = $curlResult->getHeaderArray(); + + self::assertNotEmpty($headers); + self::assertArrayHasKey('vary', $headers); + } + + /** + * @small + */ + public function testGetHeaderWithParam() + { + $header = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.head'); + $body = file_get_contents(__DIR__ . '/../../../../datasets/curl/about.body'); + + $curlResult = new CurlResult('https://test.local', $header . $body, [ + 'http_code' => 200, + 'content_type' => 'text/html; charset=utf-8', + 'url' => 'https://test.local' + ]); + + self::assertNotEmpty($curlResult->getHeaders()); + self::assertEmpty($curlResult->getHeader('wrongHeader')); + } +} diff --git a/tests/src/Network/HTTPClientTest.php b/tests/src/Network/HTTPClientTest.php deleted file mode 100644 index 0e3f74c096..0000000000 --- a/tests/src/Network/HTTPClientTest.php +++ /dev/null @@ -1,33 +0,0 @@ -setupHttpMockHandler(); - } - - /** - * Test for issue https://github.com/friendica/friendica/issues/10473#issuecomment-907749093 - */ - public function testInvalidURI() - { - $this->httpRequestHandler->setHandler(new MockHandler([ - new Response(301, ['Location' => 'https:///']), - ])); - - self::assertFalse(DI::httpClient()->get('https://friendica.local')->isSuccess()); - } -}