Failed Conditions
Pull Request — main (#3639)
by Rafael
35:01
created

PageIndexerRequest::getUrl()   A

Complexity

Conditions 3
Paths 5

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 7.3138

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 19
c 1
b 0
f 1
dl 0
loc 28
ccs 5
cts 23
cp 0.2174
rs 9.6333
cc 3
nc 5
nop 3
crap 7.3138
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;
0 ignored issues
show
Bug introduced by
The type TYPO3\CMS\Core\Utility\GeneralUtility was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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
    public const SOLR_INDEX_HEADER = 'X-Tx-Solr-Iq';
39
40
    /**
41
     * List of actions to perform during page rendering.
42
     */
43
    protected array $actions = [];
44
45
    /**
46
     * Parameters as sent from the Index Queue page indexer.
47
     */
48
    protected array $parameters = [];
49
50
    /**
51
     * Headers as sent from the Index Queue page indexer.
52
     */
53
    protected array $header = [];
54
55
    /**
56
     * Unique request ID.
57
     */
58
    protected ?string $requestId = null;
59
60
    /**
61
     * Username to use for basic auth protected URLs.
62
     */
63
    protected string $username = '';
64
65
    /**
66
     * Password to use for basic auth protected URLs.
67
     */
68
    protected string $password = '';
69
70
    /**
71
     * An Index Queue item related to this request.
72
     */
73
    protected ?Item $indexQueueItem = null;
74
75
    /**
76
     * Request timeout in seconds
77
     */
78
    protected float $timeout;
79
80
    protected SolrLogManager $logger;
81
82
    protected ExtensionConfiguration $extensionConfiguration;
83
84
    protected RequestFactory $requestFactory;
85
86
    /**
87
     * PageIndexerRequest constructor.
88
     */
89 73
    public function __construct(
90
        string $jsonEncodedParameters = null,
91
        SolrLogManager $solrLogManager = null,
92
        ExtensionConfiguration $extensionConfiguration = null,
93
        RequestFactory $requestFactory = null
94
    ) {
95 73
        $this->requestId = uniqid();
96 73
        $this->timeout = (float)ini_get('default_socket_timeout');
97
98 73
        $this->logger = $solrLogManager ?? GeneralUtility::makeInstance(SolrLogManager::class, __CLASS__);
99 73
        $this->extensionConfiguration = $extensionConfiguration ?? GeneralUtility::makeInstance(ExtensionConfiguration::class);
100 73
        $this->requestFactory = $requestFactory ?? GeneralUtility::makeInstance(RequestFactory::class);
101
102 73
        if (is_null($jsonEncodedParameters)) {
103 65
            return;
104
        }
105
106 70
        $this->parameters = (array)json_decode($jsonEncodedParameters, true);
107 70
        $this->requestId = $this->parameters['requestId'] ?? null;
108 70
        unset($this->parameters['requestId']);
109
110 70
        $actions = explode(',', $this->parameters['actions'] ?? '');
111 70
        foreach ($actions as $action) {
112 70
            $this->addAction($action);
113
        }
114 70
        unset($this->parameters['actions']);
115
    }
116
117
    /**
118
     * Adds an action to perform during page rendering.
119
     */
120 70
    public function addAction(string $actionName): void
121
    {
122 70
        $this->actions[] = $actionName;
123
    }
124
125
    /**
126
     * Executes the request.
127
     *
128
     * Uses headers to submit additional data and avoiding to have these
129
     * arguments integrated into the URL when created by RealURL.
130
     *
131
     * @throws Exception
132
     */
133 5
    public function send(string $url): PageIndexerResponse
134
    {
135
        /* @var PageIndexerResponse $response */
136 5
        $response = GeneralUtility::makeInstance(PageIndexerResponse::class);
137 5
        $decodedResponse = $this->getUrlAndDecodeResponse($url, $response);
138
139 4
        if ($decodedResponse['requestId'] != $this->requestId) {
140 1
            throw new RuntimeException(
141 1
                'Request ID mismatch. Request ID was ' . $this->requestId . ', received ' . $decodedResponse['requestId'] . '. Are requests cached?',
142 1
                1351260655
143 1
            );
144
        }
145
146 3
        $response->setRequestId($decodedResponse['requestId']);
147
148 3
        if (!is_array($decodedResponse['actionResults'])) {
149
            // nothing to parse
150
            return $response;
151
        }
152
153 3
        foreach ($decodedResponse['actionResults'] as $action => $actionResult) {
154 3
            $response->addActionResult($action, $actionResult);
155
        }
156
157 3
        return $response;
158
    }
159
160
    /**
161
     * This method is used to retrieve an url from the frontend and decode the response.
162
     *
163
     * @throws Exception
164
     */
165 5
    protected function getUrlAndDecodeResponse(string $url, PageIndexerResponse $response): bool|array
166
    {
167 5
        $headers = $this->getHeaders();
168 5
        $rawResponse = $this->getUrl($url, $headers, $this->timeout);
169
        // convert JSON response to response object properties
170 5
        $decodedResponse = $response->getResultsFromJson($rawResponse->getBody()->getContents());
171
172 5
        if ($decodedResponse === null) {
173 1
            $this->logger->log(
174 1
                SolrLogManager::ERROR,
175 1
                'Failed to execute Page Indexer Request. Request ID: ' . $this->requestId,
176 1
                [
177 1
                    'request ID' => $this->requestId,
178 1
                    'request url' => $url,
179 1
                    'request headers' => $headers,
180 1
                    'response headers' => $rawResponse->getHeaders(),
181 1
                    'raw response body' => $rawResponse->getBody()->getContents(),
182 1
                ]
183 1
            );
184
185 1
            throw new RuntimeException('Failed to execute Page Indexer Request. See log for details. Request ID: ' . $this->requestId, 1319116885);
186
        }
187 4
        return $decodedResponse;
188
    }
189
190
    /**
191
     * Generates the headers to be sent with the request.
192
     *
193
     * @return string[] Array of HTTP headers.
194
     */
195 68
    public function getHeaders(): array
196
    {
197 68
        $headers = $this->header;
198 68
        $headers[] = 'User-Agent: ' . $this->getUserAgent();
199 68
        $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

199
        /** @scrutinizer ignore-call */ 
200
        $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...
200 68
        $pageId = $this->indexQueueItem->getRecordUid();
201
202 68
        $indexerRequestData = [
203 68
            'requestId' => $this->requestId,
204 68
            'item' => $itemId,
205 68
            'page' => $pageId,
206 68
            'actions' => implode(',', $this->actions),
207 68
            'hash' => md5(
208 68
                $itemId . '|' .
209 68
                $pageId . '|' .
210 68
                $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']
211 68
            ),
212 68
        ];
213
214 68
        $indexerRequestData = array_merge($indexerRequestData, $this->parameters);
215 68
        $headers[] = self::SOLR_INDEX_HEADER . ': ' . json_encode($indexerRequestData, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
216
217 68
        return $headers;
218
    }
219
220 68
    protected function getUserAgent(): string
221
    {
222 68
        return $GLOBALS['TYPO3_CONF_VARS']['HTTP']['headers']['User-Agent'] ?? 'TYPO3';
223
    }
224
225
    /**
226
     * Adds an HTTP header to be sent with the request.
227
     */
228
    public function addHeader(string $header): void
229
    {
230
        $this->header[] = $header;
231
    }
232
233
    /**
234
     * Checks whether this is a legitimate request coming from the Index Queue
235
     * page indexer worker task.
236
     */
237 64
    public function isAuthenticated(): bool
238
    {
239 64
        $authenticated = false;
240
241 64
        if (empty($this->parameters)) {
242
            return false;
243
        }
244
245 64
        $calculatedHash = md5(
246 64
            $this->parameters['item'] . '|' .
247 64
            $this->parameters['page'] . '|' .
248 64
            $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey']
249 64
        );
250
251 64
        if ($this->parameters['hash'] === $calculatedHash) {
252 63
            $authenticated = true;
253
        }
254
255 64
        return $authenticated;
256
    }
257
258
    /**
259
     * Gets the list of actions to perform during page rendering.
260
     */
261 62
    public function getActions(): array
262
    {
263 62
        return $this->actions;
264
    }
265
266
    /**
267
     * Gets the request's parameters.
268
     */
269
    public function getParameters(): array
270
    {
271
        return $this->parameters;
272
    }
273
274
    /**
275
     * Gets the request's unique ID.
276
     */
277 63
    public function getRequestId(): ?string
278
    {
279 63
        return $this->requestId;
280
    }
281
282
    /**
283
     * Gets a specific parameter's value.
284
     *
285
     * @param string $parameterName The parameter to retrieve.
286
     *
287
     * @return mixed NULL if a parameter was not set, or it's value otherwise.
288
     */
289 63
    public function getParameter(string $parameterName): mixed
290
    {
291 63
        return $this->parameters[$parameterName] ?? null;
292
    }
293
294
    /**
295
     * Sets a request's parameter and its value.
296
     */
297 63
    public function setParameter(string $parameterName, mixed $value): void
298
    {
299 63
        if (is_bool($value)) {
300
            $value = $value ? '1' : '0';
301
        }
302
303 63
        $this->parameters[$parameterName] = $value;
304
    }
305
306
    /**
307
     * Sets username and password to be used for a basic auth request header.
308
     */
309 1
    public function setAuthorizationCredentials(string $username, string $password): void
310
    {
311 1
        $this->username = $username;
312 1
        $this->password = $password;
313
    }
314
315
    /**
316
     * Sets the Index Queue item this request is related to.
317
     */
318 68
    public function setIndexQueueItem(Item $item): void
319
    {
320 68
        $this->indexQueueItem = $item;
321
    }
322
323
    /**
324
     * Returns the request timeout in seconds
325
     */
326 1
    public function getTimeout(): float
327
    {
328 1
        return $this->timeout;
329
    }
330
331
    /**
332
     * Sets the request timeout in seconds
333
     */
334
    public function setTimeout(float $timeout): void
335
    {
336
        $this->timeout = $timeout;
337
    }
338
339
    /**
340
     * Fetches a page by sending the configured headers.
341
     *
342
     * @throws Exception
343
     */
344 1
    protected function getUrl(string $url, array $headers, float $timeout): ResponseInterface
345
    {
346 1
        $options = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $options is dead and can be removed.
Loading history...
347
        try {
348 1
            $options = $this->buildGuzzleOptions($headers, $timeout);
349 1
            $response = $this->requestFactory->request($url, 'GET', $options);
350
        } catch (ClientException|ServerException $e) {
351
            $response = $e->getResponse();
352
            if (isset($options['auth']['password'])) {
353
                $options['auth']['password'] = '*****';
354
            }
355
            // Log with INFO severity because this is what configured for Testing & Development contexts
356
            $this->logger->log(
357
                LogLevel::INFO,
358
                sprintf(
359
                    'Exception while fetching \'%s\': [%d] "%s". HTTP status: %d"',
360
                    $url,
361
                    $e->getCode(),
362
                    $e->getMessage(),
363
                    $response->getStatusCode()
364
                ),
365
                [
366
                    'HTTP headers' => $response->getHeaders(),
367
                    'options' => $options,
368
                ]
369
            );
370
        }
371 1
        return $response;
372
    }
373
374
    /**
375
     * Build the options array for the guzzle-client.
376
     */
377 1
    protected function buildGuzzleOptions(array $headers, float $timeout): array
378
    {
379 1
        $finalHeaders = [];
380
381 1
        foreach ($headers as $header) {
382 1
            [$name, $value] = explode(':', $header, 2);
383 1
            $finalHeaders[$name] = trim($value);
384
        }
385
386 1
        $options = ['headers' => $finalHeaders, 'timeout' => $timeout];
387 1
        if (!empty($this->username) && !empty($this->password)) {
388 1
            $options['auth'] = [$this->username, $this->password];
389
        }
390
391 1
        if ($this->extensionConfiguration->getIsSelfSignedCertificatesEnabled()) {
392
            $options['verify'] = false;
393
        }
394
395 1
        return $options;
396
    }
397
}
398