Passed
Push — release-11.5.x ( 385fe8...cd49eb )
by Rafael
53:22 queued 14:05
created

PageIndexerRequest::getUrl()   A

Complexity

Conditions 3
Paths 5

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 7.9297

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 18
dl 0
loc 27
ccs 4
cts 22
cp 0.1818
rs 9.6666
c 1
b 0
f 1
cc 3
nc 5
nop 3
crap 7.9297
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace ApacheSolrForTypo3\Solr\IndexQueue;
19
20
use ApacheSolrForTypo3\Solr\System\Configuration\ExtensionConfiguration;
21
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
22
use Exception;
23
use GuzzleHttp\Exception\ClientException;
24
use GuzzleHttp\Exception\ServerException;
25
use Psr\Http\Message\ResponseInterface;
26
use Psr\Log\LogLevel;
27
use RuntimeException;
28
use TYPO3\CMS\Core\Http\RequestFactory;
29
use TYPO3\CMS\Core\Utility\GeneralUtility;
30
31
/**
32
 * Index Queue Page Indexer request with details about which actions to perform.
33
 *
34
 * @author Ingo Renner <[email protected]>
35
 */
36
class PageIndexerRequest
37
{
38
    const SOLR_INDEX_HEADER = 'X-Tx-Solr-Iq';
39
40
    /**
41
     * List of actions to perform during page rendering.
42
     *
43
     * @var array
44
     */
45
    protected array $actions = [];
46
47
    /**
48
     * Parameters as sent from the Index Queue page indexer.
49
     *
50
     * @var array
51
     */
52
    protected array $parameters = [];
53
54
    /**
55
     * Headers as sent from the Index Queue page indexer.
56
     *
57
     * @var array
58
     */
59
    protected array $header = [];
60
61
    /**
62
     * Unique request ID.
63
     *
64
     * @var string|null
65
     */
66
    protected ?string $requestId = null;
67
68
    /**
69
     * Username to use for basic auth protected URLs.
70
     *
71
     * @var string
72
     */
73
    protected string $username = '';
74
75
    /**
76
     * Password to use for basic auth protected URLs.
77
     *
78
     * @var string
79
     */
80
    protected string $password = '';
81
82
    /**
83
     * An Index Queue item related to this request.
84
     *
85
     * @var Item|null
86
     */
87
    protected ?Item $indexQueueItem = null;
88
89
    /**
90
     * Request timeout in seconds
91
     *
92
     * @var float
93
     */
94
    protected float $timeout;
95
96
    /**
97
     * @var SolrLogManager
98
     */
99
    protected SolrLogManager $logger;
100
101
    /**
102
     * @var ExtensionConfiguration
103
     */
104
    protected ExtensionConfiguration $extensionConfiguration;
105
106
    /**
107
     * @var RequestFactory
108
     */
109
    protected RequestFactory $requestFactory;
110
111
    /**
112
     * PageIndexerRequest constructor.
113
     *
114
     * @param string|null $jsonEncodedParameters json encoded header
115
     * @param SolrLogManager|null $solrLogManager
116
     * @param ExtensionConfiguration|null $extensionConfiguration
117
     * @param RequestFactory|null $requestFactory
118
     */
119 21
    public function __construct(
120
        string $jsonEncodedParameters = null,
121
        SolrLogManager $solrLogManager = null,
122
        ExtensionConfiguration $extensionConfiguration = null,
123
        RequestFactory $requestFactory = null
124
    ) {
125 21
        $this->requestId = uniqid();
126 21
        $this->timeout = (float)ini_get('default_socket_timeout');
127
128 21
        $this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
129 21
        $this->extensionConfiguration = $extensionConfiguration ?? GeneralUtility::makeInstance(ExtensionConfiguration::class);
130 21
        $this->requestFactory = $requestFactory ?? GeneralUtility::makeInstance(RequestFactory::class);
131
132 21
        if (is_null($jsonEncodedParameters)) {
133 13
            return;
134
        }
135
136 8
        $this->parameters = (array)json_decode($jsonEncodedParameters, true);
137 8
        $this->requestId = $this->parameters['requestId'] ?? null;
138 8
        unset($this->parameters['requestId']);
139
140 8
        $actions = explode(',', $this->parameters['actions'] ?? '');
141 8
        foreach ($actions as $action) {
142 8
            $this->addAction($action);
143
        }
144 8
        unset($this->parameters['actions']);
145
    }
146
147
    /**
148
     * Adds an action to perform during page rendering.
149
     *
150
     * @param string $action Action name.
151
     */
152 8
    public function addAction(string $action)
153
    {
154 8
        $this->actions[] = $action;
155
    }
156
157
    /**
158
     * Executes the request.
159
     *
160
     * Uses headers to submit additional data and avoiding to have these
161
     * arguments integrated into the URL when created by RealURL.
162
     *
163
     * @param string $url The URL to request.
164
     * @return PageIndexerResponse Response
165
     * @throws Exception
166
     */
167 5
    public function send(string $url): PageIndexerResponse
168
    {
169
        /** @var $response PageIndexerResponse */
170 5
        $response = GeneralUtility::makeInstance(PageIndexerResponse::class);
171 5
        $decodedResponse = $this->getUrlAndDecodeResponse($url, $response);
172
173 4
        if ($decodedResponse['requestId'] != $this->requestId) {
174 1
            throw new RuntimeException(
175 1
                'Request ID mismatch. Request ID was ' . $this->requestId . ', received ' . $decodedResponse['requestId'] . '. Are requests cached?',
176 1
                1351260655
177 1
            );
178
        }
179
180 3
        $response->setRequestId($decodedResponse['requestId']);
181
182 3
        if (!is_array($decodedResponse['actionResults'])) {
183
            // nothing to parse
184
            return $response;
185
        }
186
187 3
        foreach ($decodedResponse['actionResults'] as $action => $actionResult) {
188 3
            $response->addActionResult($action, $actionResult);
189
        }
190
191 3
        return $response;
192
    }
193
194
    /**
195
     * This method is used to retrieve an url from the frontend and decode the response.
196
     *
197
     * @param string $url
198
     * @param PageIndexerResponse $response
199
     * @return array|bool
200
     * @throws Exception
201
     */
202 5
    protected function getUrlAndDecodeResponse(string $url, PageIndexerResponse $response)
203
    {
204 5
        $headers = $this->getHeaders();
205 5
        $rawResponse = $this->getUrl($url, $headers, $this->timeout);
206
        // convert JSON response to response object properties
207 5
        $decodedResponse = $response->getResultsFromJson($rawResponse->getBody()->getContents());
208
209 5
        if ($decodedResponse === false) {
210 1
            $this->logger->log(
211 1
                SolrLogManager::ERROR,
212 1
                'Failed to execute Page Indexer Request. Request ID: ' . $this->requestId,
213 1
                [
214 1
                    'request ID' => $this->requestId,
215 1
                    'request url' => $url,
216 1
                    'request headers' => $headers,
217 1
                    'response headers' => $rawResponse->getHeaders(),
218 1
                    'raw response body' => $rawResponse->getBody()->getContents(),
219 1
                ]
220 1
            );
221
222 1
            throw new RuntimeException('Failed to execute Page Indexer Request. See log for details. Request ID: ' . $this->requestId, 1319116885);
223
        }
224 4
        return $decodedResponse;
225
    }
226
227
    /**
228
     * Generates the headers to be sent with the request.
229
     *
230
     * @return string[] Array of HTTP headers.
231
     */
232 6
    public function getHeaders(): array
233
    {
234 6
        $headers = $this->header;
235 6
        $headers[] = 'User-Agent: ' . $this->getUserAgent();
236 6
        $itemId = $this->indexQueueItem->getIndexQueueUid();
0 ignored issues
show
Bug introduced by
The method getIndexQueueUid() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

236
        /** @scrutinizer ignore-call */ 
237
        $itemId = $this->indexQueueItem->getIndexQueueUid();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
237 6
        $pageId = $this->indexQueueItem->getRecordUid();
238
239 6
        $indexerRequestData = [
240 6
            'requestId' => $this->requestId,
241 6
            'item' => $itemId,
242 6
            'page' => $pageId,
243 6
            'actions' => implode(',', $this->actions),
244 6
            'hash' => md5(
245 6
                $itemId . '|' .
246 6
                $pageId . '|' .
247 6
                $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']
248 6
            ),
249 6
        ];
250
251 6
        $indexerRequestData = array_merge($indexerRequestData, $this->parameters);
252 6
        $headers[] = self::SOLR_INDEX_HEADER . ': ' . json_encode($indexerRequestData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
253
254 6
        return $headers;
255
    }
256
257
    /**
258
     * @return string
259
     */
260 6
    protected function getUserAgent(): string
261
    {
262 6
        return $GLOBALS['TYPO3_CONF_VARS']['HTTP']['headers']['User-Agent'] ?? 'TYPO3';
263
    }
264
265
    /**
266
     * Adds an HTTP header to be sent with the request.
267
     *
268
     * @param string $header HTTP header
269
     */
270
    public function addHeader(string $header)
271
    {
272
        $this->header[] = $header;
273
    }
274
275
    /**
276
     * Checks whether this is a legitimate request coming from the Index Queue
277
     * page indexer worker task.
278
     *
279
     * @return bool TRUE if it's a legitimate request, FALSE otherwise.
280
     */
281 2
    public function isAuthenticated(): bool
282
    {
283 2
        $authenticated = false;
284
285 2
        if (empty($this->parameters)) {
286
            return false;
287
        }
288
289 2
        $calculatedHash = md5(
290 2
            $this->parameters['item'] . '|' .
291 2
            $this->parameters['page'] . '|' .
292 2
            $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']
293 2
        );
294
295 2
        if ($this->parameters['hash'] === $calculatedHash) {
296 1
            $authenticated = true;
297
        }
298
299 2
        return $authenticated;
300
    }
301
302
    /**
303
     * Gets the list of actions to perform during page rendering.
304
     *
305
     * @return array List of actions
306
     */
307
    public function getActions(): array
308
    {
309
        return $this->actions;
310
    }
311
312
    /**
313
     * Gets the request's parameters.
314
     *
315
     * @return array Request parameters.
316
     */
317
    public function getParameters(): array
318
    {
319
        return $this->parameters;
320
    }
321
322
    /**
323
     * Gets the request's unique ID.
324
     *
325
     * @return string|null Unique request ID.
326
     */
327 1
    public function getRequestId(): ?string
328
    {
329 1
        return $this->requestId;
330
    }
331
332
    /**
333
     * Gets a specific parameter's value.
334
     *
335
     * @param string $parameterName The parameter to retrieve.
336
     * @return mixed|null NULL if a parameter was not set, or it's value otherwise.
337
     */
338 11
    public function getParameter(string $parameterName)
339
    {
340 11
        return $this->parameters[$parameterName] ?? null;
341
    }
342
343
    /**
344
     * Sets a request's parameter and its value.
345
     *
346
     * @param string $parameter Parameter name
347
     * @param mixed $value Parameter value.
348
     */
349 11
    public function setParameter(string $parameter, $value)
350
    {
351 11
        if (is_bool($value)) {
352
            $value = $value ? '1' : '0';
353
        }
354
355 11
        $this->parameters[$parameter] = $value;
356
    }
357
358
    /**
359
     * Sets username and password to be used for a basic auth request header.
360
     *
361
     * @param string $username username.
362
     * @param string $password password.
363
     */
364 1
    public function setAuthorizationCredentials(string $username, string $password)
365
    {
366 1
        $this->username = $username;
367 1
        $this->password = $password;
368
    }
369
370
    /**
371
     * Sets the Index Queue item this request is related to.
372
     *
373
     * @param Item $item Related Index Queue item.
374
     */
375 6
    public function setIndexQueueItem(Item $item)
376
    {
377 6
        $this->indexQueueItem = $item;
378
    }
379
380
    /**
381
     * Returns the request timeout in seconds
382
     *
383
     * @return float
384
     */
385 1
    public function getTimeout(): float
386
    {
387 1
        return $this->timeout;
388
    }
389
390
    /**
391
     * Sets the request timeout in seconds
392
     *
393
     * @param float $timeout Timeout seconds
394
     */
395
    public function setTimeout(float $timeout)
396
    {
397
        $this->timeout = $timeout;
398
    }
399
400
    /**
401
     * Fetches a page by sending the configured headers.
402
     *
403
     * @param string $url
404
     * @param string[] $headers
405
     * @param float $timeout
406
     * @return ResponseInterface
407
     * @throws Exception
408
     */
409 1
    protected function getUrl(string $url, array $headers, float $timeout): ResponseInterface
410
    {
411
        try {
412 1
            $options = $this->buildGuzzleOptions($headers, $timeout);
413 1
            $response = $this->requestFactory->request($url, 'GET', $options);
414
        } catch (ClientException|ServerException $e) {
415
            $response = $e->getResponse();
416
            if (isset($options['auth']['password'])) {
417
                $options['auth']['password'] = '*****';
418
            }
419
            // Log with INFO severity because this is what configured for Testing & Development contexts
420
            $this->logger->log(
421
                LogLevel::INFO,
422
                sprintf(
423
                    'Exception while fetching \'%s\': [%d] "%s". HTTP status: %d"',
424
                    $url,
425
                    $e->getCode(),
426
                    $e->getMessage(),
427
                    $response->getStatusCode()
428
                ),
429
                [
430
                    'HTTP headers' => $response->getHeaders(),
431
                    'options' => $options,
432
                ]
433
            );
434
        }
435 1
        return $response;
436
    }
437
438
    /**
439
     * Build the options array for the guzzle-client.
440
     *
441
     * @param array $headers
442
     * @param float $timeout
443
     * @return array
444
     */
445 1
    protected function buildGuzzleOptions(array $headers, float $timeout): array
446
    {
447 1
        $finalHeaders = [];
448
449 1
        foreach ($headers as $header) {
450 1
            list($name, $value) = explode(':', $header, 2);
451 1
            $finalHeaders[$name] = trim($value);
452
        }
453
454 1
        $options = ['headers' => $finalHeaders, 'timeout' => $timeout];
455 1
        if (!empty($this->username) && !empty($this->password)) {
456 1
            $options['auth'] = [$this->username, $this->password];
457
        }
458
459 1
        if ($this->extensionConfiguration->getIsSelfSignedCertificatesEnabled()) {
460
            $options['verify'] = false;
461
        }
462
463 1
        return $options;
464
    }
465
}
466