]> git.mxchange.org Git - friendica.git/commitdiff
Restructure HTTPClient for new paradigm
authorPhilipp <admin@philipp.info>
Sat, 23 Oct 2021 10:50:31 +0000 (12:50 +0200)
committerPhilipp <admin@philipp.info>
Thu, 28 Oct 2021 18:01:03 +0000 (20:01 +0200)
34 files changed:
src/Content/Text/BBCode.php
src/Core/Search.php
src/DI.php
src/Factory/HTTPClientFactory.php [deleted file]
src/Model/GServer.php
src/Model/Post/Link.php
src/Model/Post/Media.php
src/Module/Magic.php
src/Network/CurlResult.php [deleted file]
src/Network/GuzzleResponse.php [deleted file]
src/Network/HTTPClient.php [deleted file]
src/Network/HTTPClient/Capability/ICanHandleHttpResponses.php [new file with mode: 0644]
src/Network/HTTPClient/Capability/ICanRequestPerHttp.php [new file with mode: 0644]
src/Network/HTTPClient/Client/HttpClientCan.php [new file with mode: 0644]
src/Network/HTTPClient/Client/HttpClientOptions.php [new file with mode: 0644]
src/Network/HTTPClient/Factory/HttpClient.php [new file with mode: 0644]
src/Network/HTTPClient/Response/CurlResult.php [new file with mode: 0644]
src/Network/HTTPClient/Response/GuzzleResponse.php [new file with mode: 0644]
src/Network/HTTPClientOptions.php [deleted file]
src/Network/IHTTPClient.php [deleted file]
src/Network/IHTTPResult.php [deleted file]
src/Network/Probe.php
src/Protocol/OStatus.php
src/Util/HTTPSignature.php
src/Util/ParseUrl.php
src/Worker/OnePoll.php
static/dependencies.config.php
tests/DiceHttpMockHandlerTrait.php
tests/src/Core/InstallerTest.php
tests/src/Core/Storage/Repository/StorageManagerTest.php
tests/src/Network/CurlResultTest.php [deleted file]
tests/src/Network/HTTPClient/Client/HTTPClientTest.php [new file with mode: 0644]
tests/src/Network/HTTPClient/Response/CurlResultTest.php [new file with mode: 0644]
tests/src/Network/HTTPClientTest.php [deleted file]

index da95c3d67338204aea087cb1f9fef0e9f02f277b..ecaf345274ab31659a67157a1f46df9c82e55a53 100644 (file)
@@ -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 {
index ff4abdfacfb4cfcff9e7ba8c9d0696a4ae68539c..82c0ea3d36c3b0cb159ff2febb0465e76098fbca 100644 (file)
@@ -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'])) {
index 8ee7004408aecb828941f9db927aba1715678ac4..ecc65bbc98e8294089de4c51c9c9c5140da8342a 100644 (file)
@@ -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 (file)
index 18e2b0f..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-<?php
-
-namespace Friendica\Factory;
-
-use Friendica\App;
-use Friendica\BaseFactory;
-use Friendica\Core\Config\Capability\IManageConfigValues;
-use Friendica\Network\HTTPClient;
-use Friendica\Network\IHTTPClient;
-use Friendica\Util\Profiler;
-use Friendica\Util\Strings;
-use GuzzleHttp\Client;
-use GuzzleHttp\HandlerStack;
-use GuzzleHttp\RequestOptions;
-use mattwright\URLResolver;
-use Psr\Http\Message\RequestInterface;
-use Psr\Http\Message\ResponseInterface;
-use Psr\Http\Message\UriInterface;
-use Psr\Log\LoggerInterface;
-
-require_once __DIR__ . '/../../static/dbstructure.config.php';
-
-class HTTPClientFactory extends BaseFactory
-{
-       /** @var IManageConfigValues */
-       private $config;
-       /** @var Profiler */
-       private $profiler;
-       /** @var App\BaseURL */
-       private $baseUrl;
-
-       public function __construct(LoggerInterface $logger, IManageConfigValues $config, Profiler $profiler, App\BaseURL $baseUrl)
-       {
-               parent::__construct($logger);
-               $this->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);
-       }
-}
index 7c5a2b98a3fc1f0f626f6162f8cb1686bb064f30..c56e7701e0bbc225555a75a9c15485c3b8d0cf25 100644 (file)
@@ -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);
index dcda036ec13273376bcca482a90d8259b3252a94..50ed12a36999250d3ba20af6655f5d1dfca0f11a 100644 (file)
@@ -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] ?? '';
index b42ba89cf4957c66962d0ca9caa9afc6caa28a12..6fe996d76ab69e9ae384d06444f7cba7cd2e28b9 100644 (file)
@@ -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] ?? '';
index d508835b024717163ed09d7dcb232313055e769c..c51c05844cbecadf9f5d5bd27e2b35b9f0fdcc84 100644 (file)
@@ -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 (file)
index 8b3b128..0000000
+++ /dev/null
@@ -1,348 +0,0 @@
-<?php
-/**
- * @copyright Copyright (C) 2010-2021, the Friendica project
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- *
- */
-
-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 (file)
index d5afb4c..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
-<?php
-/**
- * @copyright Copyright (C) 2020, Friendica
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- *
- */
-
-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 (file)
index 004af57..0000000
+++ /dev/null
@@ -1,253 +0,0 @@
-<?php
-/**
- * @copyright Copyright (C) 2010-2021, the Friendica project
- *
- * @license GNU APGL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- *
- */
-
-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 (file)
index 0000000..b0ad0a9
--- /dev/null
@@ -0,0 +1,105 @@
+<?php
+
+namespace Friendica\Network\HTTPClient\Capability;
+
+use Psr\Http\Message\MessageInterface;
+
+/**
+ * Temporary class to map Friendica used variables based on PSR-7 HTTPResponse
+ */
+interface ICanHandleHttpResponses
+{
+       /**
+        * Gets the Return Code
+        *
+        * @return string The Return Code
+        */
+       public function getReturnCode();
+
+       /**
+        * Returns the Content Type
+        *
+        * @return string the Content Type
+        */
+       public function getContentType();
+
+       /**
+        * Returns the headers
+        *
+        * @param string $header optional header field. Return all fields if empty
+        *
+        * @return string[] the headers or the specified content of the header variable
+        *@see MessageInterface::getHeader()
+        *
+        */
+       public function getHeader(string $header);
+
+       /**
+        * Returns all headers
+        * @see MessageInterface::getHeaders()
+        *
+        * @return string[][]
+        */
+       public function getHeaders();
+
+       /**
+        * Check if a specified header exists
+        * @see MessageInterface::hasHeader()
+        *
+        * @param string $field header field
+        *
+        * @return boolean "true" if header exists
+        */
+       public function inHeader(string $field);
+
+       /**
+        * Returns the headers as an associated array
+        * @see MessageInterface::getHeaders()
+        * @deprecated
+        *
+        * @return string[][] associated header array
+        */
+       public function getHeaderArray();
+
+       /**
+        * @return bool
+        */
+       public function isSuccess();
+
+       /**
+        * @return string
+        */
+       public function getUrl();
+
+       /**
+        * @return string
+        */
+       public function getRedirectUrl();
+
+       /**
+        * @see MessageInterface::getBody()
+        *
+        * @return string
+        */
+       public function getBody();
+
+       /**
+        * @return boolean
+        */
+       public function isRedirectUrl();
+
+       /**
+        * @return integer
+        */
+       public function getErrorNumber();
+
+       /**
+        * @return string
+        */
+       public function getError();
+
+       /**
+        * @return boolean
+        */
+       public function isTimeout();
+}
diff --git a/src/Network/HTTPClient/Capability/ICanRequestPerHttp.php b/src/Network/HTTPClient/Capability/ICanRequestPerHttp.php
new file mode 100644 (file)
index 0000000..b7ddcb2
--- /dev/null
@@ -0,0 +1,133 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2021, the Friendica project
+ *
+ * @license GNU APGL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+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 (file)
index 0000000..ea07a5c
--- /dev/null
@@ -0,0 +1,258 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2021, the Friendica project
+ *
+ * @license GNU APGL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+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 (file)
index 0000000..fa684c7
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+namespace Friendica\Network\HTTPClient\Client;
+
+use GuzzleHttp\RequestOptions;
+
+/**
+ * This class contains a list of possible HTTPClient request options.
+ */
+class HttpClientOptions
+{
+       /**
+        * accept_content: (array) supply Accept: header with 'accept_content' as the value
+        */
+       const ACCEPT_CONTENT = 'accept_content';
+       /**
+        * timeout: (int) out in seconds, default system config value or 60 seconds
+        */
+       const TIMEOUT = RequestOptions::TIMEOUT;
+       /**
+        * cookiejar: (string) path to cookie jar file
+        */
+       const COOKIEJAR = 'cookiejar';
+       /**
+        * headers: (array) header array
+        */
+       const HEADERS = RequestOptions::HEADERS;
+       /**
+        * header: (array) header array (legacy version)
+        */
+       const LEGACY_HEADER = 'header';
+       /**
+        * content_length: (int) maximum File content length
+        */
+       const CONTENT_LENGTH = 'content_length';
+       /**
+        * body: (mixed) Setting the body for sending data
+        */
+       const BODY = RequestOptions::BODY;
+       /**
+        * auth: (array) Authentication settings for specific requests
+        */
+       const AUTH = RequestOptions::AUTH;
+}
diff --git a/src/Network/HTTPClient/Factory/HttpClient.php b/src/Network/HTTPClient/Factory/HttpClient.php
new file mode 100644 (file)
index 0000000..732f01b
--- /dev/null
@@ -0,0 +1,113 @@
+<?php
+
+namespace Friendica\Network\HTTPClient\Factory;
+
+use Friendica\App;
+use Friendica\BaseFactory;
+use Friendica\Core\Config\Capability\IManageConfigValues;
+use Friendica\Network\HTTPClient\Client;
+use Friendica\Network\HTTPClient\Capability\ICanRequestPerHttp;
+use Friendica\Util\Profiler;
+use Friendica\Util\Strings;
+use GuzzleHttp;
+use GuzzleHttp\HandlerStack;
+use GuzzleHttp\RequestOptions;
+use mattwright\URLResolver;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\UriInterface;
+use Psr\Log\LoggerInterface;
+
+require_once __DIR__ . '/../../../../static/dbstructure.config.php';
+
+class HttpClient extends BaseFactory
+{
+       /** @var IManageConfigValues */
+       private $config;
+       /** @var Profiler */
+       private $profiler;
+       /** @var App\BaseURL */
+       private $baseUrl;
+
+       public function __construct(LoggerInterface $logger, IManageConfigValues $config, Profiler $profiler, App\BaseURL $baseUrl)
+       {
+               parent::__construct($logger);
+               $this->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 (file)
index 0000000..adff9b8
--- /dev/null
@@ -0,0 +1,349 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2021, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+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 (file)
index 0000000..aa92309
--- /dev/null
@@ -0,0 +1,157 @@
+<?php
+/**
+ * @copyright Copyright (C) 2020, Friendica
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+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 (file)
index f9438fb..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-namespace Friendica\Network;
-
-use GuzzleHttp\RequestOptions;
-
-/**
- * This class contains a list of possible HTTPClient request options.
- */
-class HTTPClientOptions
-{
-       /**
-        * accept_content: (array) supply Accept: header with 'accept_content' as the value
-        */
-       const ACCEPT_CONTENT = 'accept_content';
-       /**
-        * timeout: (int) out in seconds, default system config value or 60 seconds
-        */
-       const TIMEOUT = RequestOptions::TIMEOUT;
-       /**
-        * cookiejar: (string) path to cookie jar file
-        */
-       const COOKIEJAR = 'cookiejar';
-       /**
-        * headers: (array) header array
-        */
-       const HEADERS = RequestOptions::HEADERS;
-       /**
-        * header: (array) header array (legacy version)
-        */
-       const LEGACY_HEADER = 'header';
-       /**
-        * content_length: (int) maximum File content length
-        */
-       const CONTENT_LENGTH = 'content_length';
-       /**
-        * body: (mixed) Setting the body for sending data
-        */
-       const BODY = RequestOptions::BODY;
-       /**
-        * auth: (array) Authentication settings for specific requests
-        */
-       const AUTH = RequestOptions::AUTH;
-}
diff --git a/src/Network/IHTTPClient.php b/src/Network/IHTTPClient.php
deleted file mode 100644 (file)
index 0b51d64..0000000
+++ /dev/null
@@ -1,133 +0,0 @@
-<?php
-/**
- * @copyright Copyright (C) 2010-2021, the Friendica project
- *
- * @license GNU APGL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- *
- */
-
-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 (file)
index 38a1176..0000000
+++ /dev/null
@@ -1,104 +0,0 @@
-<?php
-
-namespace Friendica\Network;
-
-use Psr\Http\Message\MessageInterface;
-
-/**
- * Temporary class to map Friendica used variables based on PSR-7 HTTPResponse
- */
-interface IHTTPResult
-{
-       /**
-        * Gets the Return Code
-        *
-        * @return string The Return Code
-        */
-       public function getReturnCode();
-
-       /**
-        * Returns the Content Type
-        *
-        * @return string the Content Type
-        */
-       public function getContentType();
-
-       /**
-        * Returns the headers
-        * @see MessageInterface::getHeader()
-        *
-        * @param string $header optional header field. Return all fields if empty
-        *
-        * @return string[] the headers or the specified content of the header variable
-        */
-       public function getHeader($header);
-
-       /**
-        * Returns all headers
-        * @see MessageInterface::getHeaders()
-        *
-        * @return string[][]
-        */
-       public function getHeaders();
-
-       /**
-        * Check if a specified header exists
-        * @see MessageInterface::hasHeader()
-        *
-        * @param string $field header field
-        *
-        * @return boolean "true" if header exists
-        */
-       public function inHeader(string $field);
-
-       /**
-        * Returns the headers as an associated array
-        * @see MessageInterface::getHeaders()
-        * @deprecated
-        *
-        * @return string[][] associated header array
-        */
-       public function getHeaderArray();
-
-       /**
-        * @return bool
-        */
-       public function isSuccess();
-
-       /**
-        * @return string
-        */
-       public function getUrl();
-
-       /**
-        * @return string
-        */
-       public function getRedirectUrl();
-
-       /**
-        * @see MessageInterface::getBody()
-        *
-        * @return string
-        */
-       public function getBody();
-
-       /**
-        * @return boolean
-        */
-       public function isRedirectUrl();
-
-       /**
-        * @return integer
-        */
-       public function getErrorNumber();
-
-       /**
-        * @return string
-        */
-       public function getError();
-
-       /**
-        * @return boolean
-        */
-       public function isTimeout();
-}
index 23c115eaba2035e1e405690de128a6926f07ae36..64855b83a1b6180093614af2ba9a8ea7fa2c5bf5 100644 (file)
@@ -34,6 +34,7 @@ use Friendica\Model\Contact;
 use Friendica\Model\GServer;
 use Friendica\Model\Profile;
 use Friendica\Model\User;
+use Friendica\Network\HTTPClient\Client\HttpClientOptions;
 use Friendica\Protocol\ActivityNamespace;
 use Friendica\Protocol\ActivityPub;
 use Friendica\Protocol\Email;
@@ -170,7 +171,7 @@ class Probe
                Logger::info('Probing', ['host' => $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 [];
index 46f68086b196bd667906ace246b9a40f2e0ef348..ca8a76b6b4d5b1a3e29d67ab44024c547ff573a7 100644 (file)
@@ -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;
index f11dbcceb4c20c73505d0fe9aa007e5a774f2721..36919b5b64f0273dac38089d09f6fe597562c151 100644 (file)
@@ -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);
index 58916495255e7385a63178920afc53653e591f68..6c3c4a0259e80873e1130d0b243531097f9fa3c1 100644 (file)
@@ -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;
                }
index 99d5054ca3b8727ed9077d3410abdeb1f749e62c..6d05e6cdc03286191b36f4daee66679e8e7c5135 100644 (file)
@@ -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()) {
index 042949e60e62496b2df83731f27b1067b88cb2ab..dad374d8da77c19245df6f00dc343990315fcec6 100644 (file)
@@ -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],
                ],
index 969b76b5b005f94d181aed174546756b2564226c..7f77d7e4fea1a54b859d9986dacf7b3f6309fc95 100644 (file)
@@ -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],
                        ],
index 4742d41d882853a54c95179c021855dfead3ddb5..8db5a7d5c0859d913c841bd3b6a1da1fa7916d79 100644 (file)
@@ -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);
index 0ba28af82bfa82d126d48af21a0a967241afd454..c090c26963376e869b93063cf26f27cd7da7f4cd 100644 (file)
@@ -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 (file)
index c28dc5f..0000000
+++ /dev/null
@@ -1,214 +0,0 @@
-<?php
-/**
- * @copyright Copyright (C) 2010-2021, the Friendica project
- *
- * @license GNU AGPL version 3 or any later version
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- *
- */
-
-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 (file)
index 0000000..0e2c0b3
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+
+namespace Friendica\Test\src\Network\HTTPClient\Client;
+
+use Friendica\DI;
+use Friendica\Test\DiceHttpMockHandlerTrait;
+use Friendica\Test\MockedTest;
+use GuzzleHttp\Handler\MockHandler;
+use GuzzleHttp\Psr7\Response;
+
+class HTTPClientTest extends MockedTest
+{
+       use DiceHttpMockHandlerTrait;
+
+       protected function setUp(): void
+       {
+               parent::setUp();
+
+               $this->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 (file)
index 0000000..5cc27a0
--- /dev/null
@@ -0,0 +1,214 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2021, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+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 (file)
index 0e3f74c..0000000
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-namespace Friendica\Test\src\Network;
-
-use Friendica\DI;
-use Friendica\Test\DiceHttpMockHandlerTrait;
-use Friendica\Test\MockedTest;
-use GuzzleHttp\Handler\MockHandler;
-use GuzzleHttp\Psr7\Response;
-
-class HTTPClientTest extends MockedTest
-{
-       use DiceHttpMockHandlerTrait;
-
-       protected function setUp(): void
-       {
-               parent::setUp();
-
-               $this->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());
-       }
-}