Passed
Push — release-11.5.x ( 39fc07...8ccd81 )
by Markus
34:52 queued 29:33
created

AbstractSolrService::_sendRawRequest()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5.2

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 26
ccs 12
cts 15
cp 0.8
rs 9.4555
c 0
b 0
f 0
cc 5
nc 12
nop 4
crap 5.2
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\System\Solr\Service;
19
20
use ApacheSolrForTypo3\Solr\PingFailedException;
21
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
22
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
23
use ApacheSolrForTypo3\Solr\System\Solr\ResponseAdapter;
24
use ApacheSolrForTypo3\Solr\Util;
25
use Closure;
26
use Solarium\Client;
27
use Solarium\Core\Client\Endpoint;
28
use Solarium\Core\Client\Request;
29
use Solarium\Core\Query\QueryInterface;
30
use Solarium\Exception\HttpException;
31
use Throwable;
32
use TYPO3\CMS\Core\Http\Uri;
33
use TYPO3\CMS\Core\Utility\GeneralUtility;
34
35
abstract class AbstractSolrService
36
{
37
    /**
38
     * @var array
39
     */
40
    protected static array $pingCache = [];
41
42
    /**
43
     * @var TypoScriptConfiguration
44
     */
45
    protected TypoScriptConfiguration $configuration;
46
47
    /**
48
     * @var SolrLogManager
49
     */
50
    protected SolrLogManager $logger;
51
52
    /**
53
     * @var Client
54
     */
55
    protected Client $client;
56
57
    /**
58
     * SolrReadService constructor.
59
     */
60 142
    public function __construct(Client $client, $typoScriptConfiguration = null, $logManager = null)
61
    {
62 142
        $this->client = $client;
63 142
        $this->configuration = $typoScriptConfiguration ?? Util::getSolrConfiguration();
64 142
        $this->logger = $logManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
65
    }
66
67
    /**
68
     * Returns the path to the core solr path + core path.
69
     *
70
     * @return string
71
     */
72 2
    public function getCorePath(): string
73
    {
74 2
        $endpoint = $this->getPrimaryEndpoint();
75 2
        return $endpoint->getPath() . '/' . $endpoint->getCore();
76
    }
77
78
    /**
79
     * Returns the Solarium client
80
     *
81
     * @return ?Client
82
     */
83 1
    public function getClient(): ?Client
84
    {
85 1
        return $this->client;
86
    }
87
88
    /**
89
     * Return a valid http URL given this server's host, port and path and a provided servlet name
90
     *
91
     * @param string $servlet
92
     * @param array $params
93
     * @return string
94
     */
95 27
    protected function _constructUrl(string $servlet, array $params = []): string
96
    {
97 27
        $queryString = count($params) ? '?' . http_build_query($params) : '';
98 27
        return $this->__toString() . $servlet . $queryString;
99
    }
100
101
    /**
102
     * Creates a string representation of the Solr connection. Specifically
103
     * will return the Solr URL.
104
     *
105
     * @return string The Solr URL.
106
     * @TODO: Add support for API version 2
107
     */
108 100
    public function __toString()
109
    {
110 100
        $endpoint = $this->getPrimaryEndpoint();
111
        try {
112 100
            return $endpoint->getCoreBaseUri();
113
        } catch (Throwable $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
114
        }
115
        return  $endpoint->getScheme() . '://' . $endpoint->getHost() . ':' . $endpoint->getPort() . $endpoint->getPath() . '/' . $endpoint->getCore() . '/';
116
    }
117
118
    /**
119
     * @return Endpoint|null
120
     */
121 105
    public function getPrimaryEndpoint(): Endpoint
122
    {
123 105
        return $this->client->getEndpoint();
124
    }
125
126
    /**
127
     * Central method for making a get operation against this Solr Server
128
     *
129
     * @param string $url
130
     * @return ResponseAdapter
131
     */
132 22
    protected function _sendRawGet(string $url): ResponseAdapter
133
    {
134 22
        return $this->_sendRawRequest($url);
135
    }
136
137
    /**
138
     * Central method for making an HTTP DELETE operation against the Solr server
139
     *
140
     * @param string $url
141
     * @return ResponseAdapter
142
     */
143 9
    protected function _sendRawDelete(string $url): ResponseAdapter
144
    {
145 9
        return $this->_sendRawRequest($url, Request::METHOD_DELETE);
146
    }
147
148
    /**
149
     * Central method for making a post operation against this Solr Server
150
     *
151
     * @param string $url
152
     * @param string $rawPost
153
     * @param string $contentType
154
     * @return ResponseAdapter
155
     */
156 9
    protected function _sendRawPost(
157
        string $url,
158
        string $rawPost,
159
        string $contentType = 'text/xml; charset=UTF-8'
160
    ): ResponseAdapter {
161 9
        $initializeRequest = function (Request $request) use ($rawPost, $contentType) {
162 9
            $request->setRawData($rawPost);
163 9
            $request->addHeader('Content-Type: ' . $contentType);
164 9
            return $request;
165 9
        };
166
167 9
        return $this->_sendRawRequest($url, Request::METHOD_POST, $rawPost, $initializeRequest);
168
    }
169
170
    /**
171
     * Method that performs an HTTP request with the solarium client.
172
     *
173
     * @param string $url
174
     * @param string $method
175
     * @param string $body
176
     * @param ?Closure $initializeRequest
177
     * @return ResponseAdapter
178
     */
179 22
    protected function _sendRawRequest(
180
        string $url,
181
        string $method = Request::METHOD_GET,
182
        string $body = '',
183
        Closure $initializeRequest = null
184
    ): ResponseAdapter {
185 22
        $logSeverity = SolrLogManager::INFO;
186 22
        $exception = null;
187 22
        $url = $this->reviseUrl($url);
188
        try {
189 22
            $request = $this->buildSolariumRequestFromUrl($url, $method);
190 22
            if ($initializeRequest !== null) {
191 9
                $request = $initializeRequest($request);
192
            }
193 22
            $response = $this->executeRequest($request);
194
        } catch (HttpException $exception) {
195
            $logSeverity = SolrLogManager::ERROR;
196
            $response = new ResponseAdapter($exception->getBody(), $exception->getCode(), $exception->getMessage());
197
        }
198
199 22
        if ($this->configuration->getLoggingQueryRawPost() || $response->getHttpStatus() != 200) {
200 8
            $message = 'Querying Solr using ' . $method;
201 8
            $this->writeLog($logSeverity, $message, $url, $response, $exception, $body);
202
        }
203
204 22
        return $response;
205
    }
206
207
    /**
208
     * Revise url
209
     * - Resolve relative paths
210
     *
211
     * @param string $url
212
     * @return string
213
     */
214 22
    protected function reviseUrl(string $url): string
215
    {
216
        /* @var Uri $uri */
217 22
        $uri = GeneralUtility::makeInstance(Uri::class, $url);
218
219 22
        if ($uri->getPath() === '') {
220
            return $url;
221
        }
222
223 22
        $path = trim($uri->getPath(), '/');
224 22
        $pathsCurrent = explode('/', $path);
225 22
        $pathNew = [];
226 22
        foreach ($pathsCurrent as $pathCurrent) {
227 22
            if ($pathCurrent === '..') {
228 11
                array_pop($pathNew);
229 11
                continue;
230
            }
231 22
            if ($pathCurrent === '.') {
232
                continue;
233
            }
234 22
            $pathNew[] = $pathCurrent;
235
        }
236
237 22
        $uri = $uri->withPath(implode('/', $pathNew));
238 22
        return (string)$uri;
239
    }
240
241
    /**
242
     * Build the log data and writes the message to the log
243
     *
244
     * @param string $logSeverity
245
     * @param string $message
246
     * @param string $url
247
     * @param ResponseAdapter|null $solrResponse
248
     * @param ?Throwable $exception
249
     * @param string $contentSend
250
     */
251 8
    protected function writeLog(
252
        string $logSeverity,
253
        string $message,
254
        string $url,
255
        ?ResponseAdapter $solrResponse,
256
        Throwable $exception = null,
257
        string $contentSend = ''
258
    ) {
259 8
        $logData = $this->buildLogDataFromResponse($solrResponse, $exception, $url, $contentSend);
0 ignored issues
show
Bug introduced by
It seems like $solrResponse can also be of type null; however, parameter $solrResponse of ApacheSolrForTypo3\Solr\...ldLogDataFromResponse() does only seem to accept ApacheSolrForTypo3\Solr\...em\Solr\ResponseAdapter, maybe add an additional type check? ( Ignorable by Annotation )

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

259
        $logData = $this->buildLogDataFromResponse(/** @scrutinizer ignore-type */ $solrResponse, $exception, $url, $contentSend);
Loading history...
260 8
        $this->logger->log($logSeverity, $message, $logData);
261
    }
262
263
    /**
264
     * Parses the solr information to build data for the logger.
265
     *
266
     * @param ResponseAdapter $solrResponse
267
     * @param ?Throwable $e
268
     * @param string $url
269
     * @param string $contentSend
270
     * @return array
271
     */
272 8
    protected function buildLogDataFromResponse(
273
        ResponseAdapter $solrResponse,
274
        Throwable $e = null,
275
        string $url = '',
276
        string $contentSend = ''
277
    ): array {
278 8
        $logData = ['query url' => $url, 'response' => (array)$solrResponse];
279
280 8
        if ($contentSend !== '') {
281
            $logData['content'] = $contentSend;
282
        }
283
284 8
        if (!empty($e)) {
285
            $logData['exception'] = $e->__toString();
286
            return $logData;
287
        }
288
        // trigger data parsing
289
        /**
290
         * @noinspection PhpExpressionResultUnusedInspection
291
         * @extensionScannerIgnoreLine
292
         */
293 8
        $solrResponse->response;
294 8
        $logData['response data'] = print_r($solrResponse, true);
295 8
        return $logData;
296
    }
297
298
    /**
299
     * Call the /admin/ping servlet, can be used to quickly tell if a connection to the
300
     * server is available.
301
     *
302
     * Simply overrides the SolrPhpClient implementation, changing ping from a
303
     * HEAD to a GET request, see http://forge.typo3.org/issues/44167
304
     *
305
     * Also does not report the time, see https://forge.typo3.org/issues/64551
306
     *
307
     * @param bool $useCache indicates if the ping result should be cached in the instance or not
308
     * @return bool TRUE if Solr can be reached, FALSE if not
309
     */
310 75
    public function ping(bool $useCache = true): bool
311
    {
312
        try {
313 75
            $httpResponse = $this->performPingRequest($useCache);
314
        } catch (HttpException $exception) {
315
            return false;
316
        }
317
318 75
        return $httpResponse->getHttpStatus() === 200;
319
    }
320
321
    /**
322
     * Call the /admin/ping servlet, can be used to get the runtime of a ping request.
323
     *
324
     * @param bool $useCache indicates if the ping result should be cached in the instance or not
325
     * @return float runtime in milliseconds
326
     * @throws PingFailedException
327
     */
328 3
    public function getPingRoundTripRuntime(bool $useCache = true): float
329
    {
330
        try {
331 3
            $start = $this->getMilliseconds();
332 3
            $httpResponse = $this->performPingRequest($useCache);
333 3
            $end = $this->getMilliseconds();
334
        } catch (HttpException $e) {
335
            throw new PingFailedException(
336
                'Solr ping failed with unexpected response code: ' . $e->getCode(),
337
                1645716101
338
            );
339
        }
340
341 3
        if ($httpResponse->getHttpStatus() !== 200) {
342 1
            throw new PingFailedException(
343 1
                'Solr ping failed with unexpected response code: ' . $httpResponse->getHttpStatus(),
344 1
                1645716102
345 1
            );
346
        }
347
348 2
        return $end - $start;
349
    }
350
351
    /**
352
     * Performs a ping request and returns the result.
353
     *
354
     * @param bool $useCache indicates if the ping result should be cached in the instance or not
355
     * @return ResponseAdapter
356
     */
357 78
    protected function performPingRequest(bool $useCache = true): ResponseAdapter
358
    {
359 78
        $cacheKey = (string)($this);
360 78
        if ($useCache && isset(static::$pingCache[$cacheKey])) {
361 69
            return static::$pingCache[$cacheKey];
362
        }
363
364 78
        $pingQuery = $this->client->createPing();
365 78
        $pingResult = $this->createAndExecuteRequest($pingQuery);
366
367 78
        if ($useCache) {
368 77
            static::$pingCache[$cacheKey] = $pingResult;
369
        }
370
371 78
        return $pingResult;
372
    }
373
374
    /**
375
     * Returns the current time in milliseconds.
376
     *
377
     * @return float
378
     */
379 3
    protected function getMilliseconds(): float
380
    {
381 3
        return round(microtime(true) * 1000);
382
    }
383
384
    /**
385
     * @param QueryInterface $query
386
     * @return ResponseAdapter
387
     */
388 107
    protected function createAndExecuteRequest(QueryInterface $query): ResponseAdapter
389
    {
390 107
        $request = $this->client->createRequest($query);
391 107
        return $this->executeRequest($request);
392
    }
393
394
    /**
395
     * @param Request $request
396
     * @return ResponseAdapter
397
     */
398 128
    protected function executeRequest(Request $request): ResponseAdapter
399
    {
400
        try {
401 128
            $result = $this->client->executeRequest($request);
402 4
        } catch (HttpException $e) {
403 4
            return new ResponseAdapter($e->getMessage(), $e->getCode(), $e->getStatusMessage());
404
        }
405
406 124
        return new ResponseAdapter($result->getBody(), $result->getStatusCode(), $result->getStatusMessage());
407
    }
408
409
    /**
410
     * Build the request for Solarium.
411
     *
412
     * Important: The endpoint already contains the API information.
413
     * The internal Solarium will append the information including the core if set.
414
     *
415
     * @param string $url
416
     * @param string $httpMethod
417
     * @return Request
418
     */
419 22
    protected function buildSolariumRequestFromUrl(
420
        string $url,
421
        string $httpMethod = Request::METHOD_GET
422
    ): Request {
423 22
        $params = [];
424 22
        parse_str(parse_url($url, PHP_URL_QUERY) ?? '', $params);
425 22
        $request = new Request();
426 22
        $path = parse_url($url, PHP_URL_PATH) ?? '';
427 22
        $endpoint = $this->getPrimaryEndpoint();
428 22
        $api = $request->getApi() === Request::API_V1 ? 'solr' : 'api';
429 22
        $coreBasePath = $endpoint->getPath() . '/' . $api . '/' . $endpoint->getCore() . '/';
430
431 22
        $handler = $this->buildRelativePath($coreBasePath, $path);
432 22
        $request->setMethod($httpMethod);
433 22
        $request->setParams($params);
434 22
        $request->setHandler($handler);
435 22
        return $request;
436
    }
437
438
    /**
439
     * Build a relative path from base path to target path.
440
     * Required since Solarium contains the core information
441
     *
442
     * @param string $basePath
443
     * @param string $targetPath
444
     * @return string
445
     */
446 22
    protected function buildRelativePath(string $basePath, string $targetPath): string
447
    {
448 22
        $basePath = trim($basePath, '/');
449 22
        $targetPath = trim($targetPath, '/');
450 22
        $baseElements = explode('/', $basePath);
451 22
        $targetElements = explode('/', $targetPath);
452 22
        $targetSegment = array_pop($targetElements);
453 22
        foreach ($baseElements as $i => $segment) {
454 22
            if (isset($targetElements[$i]) && $segment === $targetElements[$i]) {
455 22
                unset($baseElements[$i], $targetElements[$i]);
456
            } else {
457 11
                break;
458
            }
459
        }
460 22
        $targetElements[] = $targetSegment;
461 22
        return str_repeat('../', count($baseElements)) . implode('/', $targetElements);
462
    }
463
}
464