]> git.mxchange.org Git - friendica.git/blob - src/Network/HTTPClient/Response/CurlResult.php
Update function / rearrange tab order
[friendica.git] / src / Network / HTTPClient / Response / CurlResult.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2010-2023, the Friendica project
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
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.
11  *
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.
16  *
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/>.
19  *
20  */
21
22 namespace Friendica\Network\HTTPClient\Response;
23
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;
29
30 /**
31  * A content class for Curl call results
32  */
33 class CurlResult implements ICanHandleHttpResponses
34 {
35         /**
36          * @var int HTTP return code or 0 if timeout or failure
37          */
38         private $returnCode;
39
40         /**
41          * @var string the content type of the Curl call
42          */
43         private $contentType;
44
45         /**
46          * @var string the HTTP headers of the Curl call
47          */
48         private $header;
49
50         /**
51          * @var array the HTTP headers of the Curl call
52          */
53         private $header_fields;
54
55         /**
56          * @var boolean true (if HTTP 2xx result) or false
57          */
58         private $isSuccess;
59
60         /**
61          * @var string the URL which was called
62          */
63         private $url;
64
65         /**
66          * @var string in case of redirect, content was finally retrieved from this URL
67          */
68         private $redirectUrl;
69
70         /**
71          * @var string fetched content
72          */
73         private $body;
74
75         /**
76          * @var array some informations about the fetched data
77          */
78         private $info;
79
80         /**
81          * @var boolean true if the URL has a redirect
82          */
83         private $isRedirectUrl;
84
85         /**
86          * @var boolean true if the curl request timed out
87          */
88         private $isTimeout;
89
90         /**
91          * @var int the error number or 0 (zero) if no error
92          */
93         private $errorNumber;
94
95         /**
96          * @var string the error message or '' (the empty string) if no
97          */
98         private $error;
99
100         /**
101          * @var LoggerInterface
102          */
103         protected $logger;
104
105         /**
106          * Creates an errored CURL response
107          *
108          * @param string $url optional URL
109          *
110          * @return ICanHandleHttpResponses a CURL with error response
111          * @throws UnprocessableEntityException
112          */
113         public static function createErrorCurl(LoggerInterface $logger, string $url = '')
114         {
115                 return new CurlResult($logger, $url, '', ['http_code' => 0]);
116         }
117
118         /**
119          * Curl constructor.
120          *
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
126          *
127          * @throws UnprocessableEntityException when HTTP code of the CURL response is missing
128          */
129         public function __construct(LoggerInterface $logger, string $url, string $result, array $info, int $errorNumber = 0, string $error = '')
130         {
131                 $this->logger = $logger;
132
133                 if (!array_key_exists('http_code', $info)) {
134                         throw new UnprocessableEntityException('CURL response doesn\'t contains a response HTTP code');
135                 }
136
137                 $this->returnCode  = $info['http_code'];
138                 $this->url         = $url;
139                 $this->info        = $info;
140                 $this->errorNumber = $errorNumber;
141                 $this->error       = $error;
142
143                 $this->logger->debug('construct', ['url' => $url, 'returncode' => $this->returnCode, 'result' => $result]);
144
145                 $this->parseBodyHeader($result);
146                 $this->checkSuccess();
147                 $this->checkRedirect();
148                 $this->checkInfo();
149         }
150
151         private function parseBodyHeader($result)
152         {
153                 // Pull out multiple headers, e.g. proxy and continuation headers
154                 // allow for HTTP/2.x without fixing code
155
156                 $header = '';
157                 $base   = $result;
158                 while (preg_match('/^HTTP\/.+? \d+/', $base)) {
159                         $chunk = substr($base, 0, strpos($base, "\r\n\r\n") + 4);
160                         $header .= $chunk;
161                         $base = substr($base, strlen($chunk));
162                 }
163
164                 $this->body          = substr($result, strlen($header));
165                 $this->header        = $header;
166                 $this->header_fields = []; // Is filled on demand
167         }
168
169         private function checkSuccess()
170         {
171                 $this->isSuccess = ($this->returnCode >= 200 && $this->returnCode <= 299) || $this->errorNumber == 0;
172
173                 // Everything higher or equal 400 is not a success
174                 if ($this->returnCode >= 400) {
175                         $this->isSuccess = false;
176                 }
177
178                 if (empty($this->returnCode) && empty($this->header) && empty($this->body)) {
179                         $this->isSuccess = false;
180                 }
181
182                 if (!$this->isSuccess) {
183                         $this->logger->debug('debug', ['info' => $this->info]);
184                 }
185
186                 if (!$this->isSuccess && $this->errorNumber == CURLE_OPERATION_TIMEDOUT) {
187                         $this->isTimeout = true;
188                 } else {
189                         $this->isTimeout = false;
190                 }
191         }
192
193         private function checkRedirect()
194         {
195                 if (!array_key_exists('url', $this->info)) {
196                         $this->redirectUrl = '';
197                 } else {
198                         $this->redirectUrl = $this->info['url'];
199                 }
200
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 = [];
205                         }
206
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);
211                                 }
212                         }
213
214                         $parts = parse_url($this->info['url'] ?? '');
215                         if (empty($parts)) {
216                                 $parts = [];
217                         }
218
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];
224                                 }
225                         }
226
227                         $this->redirectUrl = Network::unparseURL($redirect_parts);
228
229                         $this->isRedirectUrl = true;
230                 } else {
231                         $this->isRedirectUrl = false;
232                 }
233         }
234
235         private function checkInfo()
236         {
237                 if (isset($this->info['content_type'])) {
238                         $this->contentType = $this->info['content_type'];
239                 } else {
240                         $this->contentType = '';
241                 }
242         }
243
244         /** {@inheritDoc} */
245         public function getReturnCode(): string
246         {
247                 return $this->returnCode;
248         }
249
250         /** {@inheritDoc} */
251         public function getContentType(): string
252         {
253                 return $this->contentType;
254         }
255
256         /** {@inheritDoc} */
257         public function getHeader(string $header): array
258         {
259                 if (empty($header)) {
260                         return [];
261                 }
262
263                 $header = strtolower(trim($header));
264
265                 $headers = $this->getHeaderArray();
266
267                 if (isset($headers[$header])) {
268                         return $headers[$header];
269                 }
270
271                 return [];
272         }
273
274         /** {@inheritDoc} */
275         public function getHeaders(): array
276         {
277                 return $this->getHeaderArray();
278         }
279
280         /** {@inheritDoc} */
281         public function inHeader(string $field): bool
282         {
283                 $field = strtolower(trim($field));
284
285                 $headers = $this->getHeaderArray();
286
287                 return array_key_exists($field, $headers);
288         }
289
290         /** {@inheritDoc} */
291         public function getHeaderArray(): array
292         {
293                 if (!empty($this->header_fields)) {
294                         return $this->header_fields;
295                 }
296
297                 $this->header_fields = [];
298
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;
308                         }
309                 }
310
311                 return $this->header_fields;
312         }
313
314         /** {@inheritDoc} */
315         public function isSuccess(): bool
316         {
317                 return $this->isSuccess;
318         }
319
320         /** {@inheritDoc} */
321         public function getUrl(): string
322         {
323                 return $this->url;
324         }
325
326         /** {@inheritDoc} */
327         public function getRedirectUrl(): string
328         {
329                 return $this->redirectUrl;
330         }
331
332         /** {@inheritDoc} */
333         public function getBody(): string
334         {
335                 return $this->body;
336         }
337
338         /** {@inheritDoc} */
339         public function isRedirectUrl(): bool
340         {
341                 return $this->isRedirectUrl;
342         }
343
344         /** {@inheritDoc} */
345         public function getErrorNumber(): int
346         {
347                 return $this->errorNumber;
348         }
349
350         /** {@inheritDoc} */
351         public function getError(): string
352         {
353                 return $this->error;
354         }
355
356         /** {@inheritDoc} */
357         public function isTimeout(): bool
358         {
359                 return $this->isTimeout;
360         }
361 }