3 * @copyright Copyright (C) 2010-2023, the Friendica project
5 * @license GNU AGPL version 3 or any later version
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 namespace Friendica\Network\HTTPClient\Response;
24 use Friendica\Core\Logger;
25 use Friendica\Network\HTTPClient\Capability\ICanHandleHttpResponses;
26 use Friendica\Network\HTTPException\UnprocessableEntityException;
27 use Friendica\Util\Network;
28 use Psr\Log\LoggerInterface;
31 * A content class for Curl call results
33 class CurlResult implements ICanHandleHttpResponses
36 * @var int HTTP return code or 0 if timeout or failure
41 * @var string the content type of the Curl call
46 * @var string the HTTP headers of the Curl call
51 * @var array the HTTP headers of the Curl call
53 private $header_fields;
56 * @var boolean true (if HTTP 2xx result) or false
61 * @var string the URL which was called
66 * @var string in case of redirect, content was finally retrieved from this URL
71 * @var string fetched content
76 * @var array some informations about the fetched data
81 * @var boolean true if the URL has a redirect
83 private $isRedirectUrl;
86 * @var boolean true if the curl request timed out
91 * @var int the error number or 0 (zero) if no error
96 * @var string the error message or '' (the empty string) if no
101 * @var LoggerInterface
106 * Creates an errored CURL response
108 * @param string $url optional URL
110 * @return ICanHandleHttpResponses a CURL with error response
111 * @throws UnprocessableEntityException
113 public static function createErrorCurl(LoggerInterface $logger, string $url = '')
115 return new CurlResult($logger, $url, '', ['http_code' => 0]);
121 * @param string $url the URL which was called
122 * @param string $result the result of the curl execution
123 * @param array $info an additional info array
124 * @param int $errorNumber the error number or 0 (zero) if no error
125 * @param string $error the error message or '' (the empty string) if no
127 * @throws UnprocessableEntityException when HTTP code of the CURL response is missing
129 public function __construct(LoggerInterface $logger, string $url, string $result, array $info, int $errorNumber = 0, string $error = '')
131 $this->logger = $logger;
133 if (!array_key_exists('http_code', $info)) {
134 throw new UnprocessableEntityException('CURL response doesn\'t contains a response HTTP code');
137 $this->returnCode = $info['http_code'];
140 $this->errorNumber = $errorNumber;
141 $this->error = $error;
143 $this->logger->debug('construct', ['url' => $url, 'returncode' => $this->returnCode, 'result' => $result]);
145 $this->parseBodyHeader($result);
146 $this->checkSuccess();
147 $this->checkRedirect();
151 private function parseBodyHeader($result)
153 // Pull out multiple headers, e.g. proxy and continuation headers
154 // allow for HTTP/2.x without fixing code
158 while (preg_match('/^HTTP\/.+? \d+/', $base)) {
159 $chunk = substr($base, 0, strpos($base, "\r\n\r\n") + 4);
161 $base = substr($base, strlen($chunk));
164 $this->body = substr($result, strlen($header));
165 $this->header = $header;
166 $this->header_fields = []; // Is filled on demand
169 private function checkSuccess()
171 $this->isSuccess = ($this->returnCode >= 200 && $this->returnCode <= 299) || $this->errorNumber == 0;
173 // Everything higher or equal 400 is not a success
174 if ($this->returnCode >= 400) {
175 $this->isSuccess = false;
178 if (empty($this->returnCode) && empty($this->header) && empty($this->body)) {
179 $this->isSuccess = false;
182 if (!$this->isSuccess) {
183 $this->logger->debug('debug', ['info' => $this->info]);
186 if (!$this->isSuccess && $this->errorNumber == CURLE_OPERATION_TIMEDOUT) {
187 $this->isTimeout = true;
189 $this->isTimeout = false;
193 private function checkRedirect()
195 if (!array_key_exists('url', $this->info)) {
196 $this->redirectUrl = '';
198 $this->redirectUrl = $this->info['url'];
201 if ($this->returnCode == 301 || $this->returnCode == 302 || $this->returnCode == 303 || $this->returnCode == 307) {
202 $redirect_parts = parse_url($this->info['redirect_url'] ?? '');
203 if (empty($redirect_parts)) {
204 $redirect_parts = [];
207 if (preg_match('/(Location:|URI:)(.*?)\n/i', $this->header, $matches)) {
208 $redirect_parts2 = parse_url(trim(array_pop($matches)));
209 if (!empty($redirect_parts2)) {
210 $redirect_parts = array_merge($redirect_parts, $redirect_parts2);
214 $parts = parse_url($this->info['url'] ?? '');
219 /// @todo Checking the corresponding RFC which parts of a redirect can be omitted.
220 $components = ['scheme', 'host', 'path', 'query', 'fragment'];
221 foreach ($components as $component) {
222 if (empty($redirect_parts[$component]) && !empty($parts[$component])) {
223 $redirect_parts[$component] = $parts[$component];
227 $this->redirectUrl = Network::unparseURL($redirect_parts);
229 $this->isRedirectUrl = true;
231 $this->isRedirectUrl = false;
235 private function checkInfo()
237 if (isset($this->info['content_type'])) {
238 $this->contentType = $this->info['content_type'];
240 $this->contentType = '';
245 public function getReturnCode(): string
247 return $this->returnCode;
251 public function getContentType(): string
253 return $this->contentType;
257 public function getHeader(string $header): array
259 if (empty($header)) {
263 $header = strtolower(trim($header));
265 $headers = $this->getHeaderArray();
267 if (isset($headers[$header])) {
268 return $headers[$header];
275 public function getHeaders(): array
277 return $this->getHeaderArray();
281 public function inHeader(string $field): bool
283 $field = strtolower(trim($field));
285 $headers = $this->getHeaderArray();
287 return array_key_exists($field, $headers);
291 public function getHeaderArray(): array
293 if (!empty($this->header_fields)) {
294 return $this->header_fields;
297 $this->header_fields = [];
299 $lines = explode("\n", trim($this->header));
300 foreach ($lines as $line) {
301 $parts = explode(':', $line);
302 $headerfield = strtolower(trim(array_shift($parts)));
303 $headerdata = trim(implode(':', $parts));
304 if (empty($this->header_fields[$headerfield])) {
305 $this->header_fields[$headerfield] = [$headerdata];
306 } elseif (!in_array($headerdata, $this->header_fields[$headerfield])) {
307 $this->header_fields[$headerfield][] = $headerdata;
311 return $this->header_fields;
315 public function isSuccess(): bool
317 return $this->isSuccess;
321 public function getUrl(): string
327 public function getRedirectUrl(): string
329 return $this->redirectUrl;
333 public function getBody(): string
339 public function isRedirectUrl(): bool
341 return $this->isRedirectUrl;
345 public function getErrorNumber(): int
347 return $this->errorNumber;
351 public function getError(): string
357 public function isTimeout(): bool
359 return $this->isTimeout;