Passed
Push — master ( fb70ba...785b5c )
by
unknown
34:59
created

AbstractSolrService::ping()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 5
c 2
b 0
f 0
dl 0
loc 9
ccs 3
cts 5
cp 0.6
rs 10
cc 2
nc 2
nop 1
crap 2.2559
1
<?php
2
namespace ApacheSolrForTypo3\Solr\System\Solr\Service;
3
4
/***************************************************************
5
 *  Copyright notice
6
 *
7
 *  (c) 2009-2017 Timo Hund <[email protected]>
8
 *  All rights reserved
9
 *
10
 *  This script is part of the TYPO3 project. The TYPO3 project is
11
 *  free software; you can redistribute it and/or modify
12
 *  it under the terms of the GNU General Public License as published by
13
 *  the Free Software Foundation; either version 3 of the License, or
14
 *  (at your option) any later version.
15
 *
16
 *  The GNU General Public License can be found at
17
 *  http://www.gnu.org/copyleft/gpl.html.
18
 *
19
 *  This script is distributed in the hope that it will be useful,
20
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
21
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22
 *  GNU General Public License for more details.
23
 *
24
 *  This copyright notice MUST APPEAR in all copies of the script!
25
 ***************************************************************/
26
27
use ApacheSolrForTypo3\Solr\PingFailedException;
28
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
29
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
30
use ApacheSolrForTypo3\Solr\System\Solr\ResponseAdapter;
31
use ApacheSolrForTypo3\Solr\Util;
32
use Solarium\Client;
33
use Solarium\Core\Client\Endpoint;
34
use Solarium\Core\Client\Request;
35
use Solarium\Core\Query\QueryInterface;
36
use Solarium\Exception\HttpException;
37
use TYPO3\CMS\Core\Http\Uri;
38
use TYPO3\CMS\Core\Utility\GeneralUtility;
39
40
abstract class AbstractSolrService
41
{
42
43
    /**
44
     * @var array
45
     */
46
    protected static $pingCache = [];
47
48
    /**
49
     * @var TypoScriptConfiguration
50
     */
51
    protected $configuration;
52
53
    /**
54
     * @var \ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager
55
     */
56
    protected $logger = null;
57
58
    /**
59
     * @var Client
60
     */
61
    protected $client = null;
62
63
    /**
64
     * SolrReadService constructor.
65
     */
66 91
    public function __construct(Client $client, $typoScriptConfiguration = null, $logManager = null)
67
    {
68 91
        $this->client = $client;
69 91
        $this->configuration = $typoScriptConfiguration ?? Util::getSolrConfiguration();
70 91
        $this->logger = $logManager ?? GeneralUtility::makeInstance(SolrLogManager::class, /** @scrutinizer ignore-type */ __CLASS__);
71 91
    }
72
73
    /**
74
     * Returns the path to the core solr path + core path.
75
     *
76
     * @return string
77
     */
78 2
    public function getCorePath()
79
    {
80 2
        $endpoint = $this->getPrimaryEndpoint();
81 2
        return is_null($endpoint) ? '' : $endpoint->getPath() .'/'. $endpoint->getCore();
82
    }
83
84
    /**
85
     * Returns the Solarium client
86
     *
87
     * @return ?Client
88
     */
89 1
    public function getClient(): ?Client
90
    {
91 1
        return $this->client;
92
    }
93
94
    /**
95
     * Return a valid http URL given this server's host, port and path and a provided servlet name
96
     *
97
     * @param string $servlet
98
     * @param array $params
99
     * @return string
100
     */
101 18
    protected function _constructUrl($servlet, $params = [])
102
    {
103 18
        $queryString = count($params) ? '?' . http_build_query($params, null, '&') : '';
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type string expected by parameter $numeric_prefix of http_build_query(). ( Ignorable by Annotation )

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

103
        $queryString = count($params) ? '?' . http_build_query($params, /** @scrutinizer ignore-type */ null, '&') : '';
Loading history...
104 18
        return $this->__toString() . $servlet . $queryString;
105
    }
106
107
    /**
108
     * Creates a string representation of the Solr connection. Specifically
109
     * will return the Solr URL.
110
     *
111
     * @return string The Solr URL.
112
     * @TODO: Add support for API version 2
113
     */
114 53
    public function __toString()
115
    {
116 53
        $endpoint = $this->getPrimaryEndpoint();
117 53
        if (!$endpoint instanceof Endpoint) {
118 2
            return '';
119
        }
120
121
        try {
122 51
            return $endpoint->getCoreBaseUri();
123
        } catch (\Exception $exception) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
124
        }
125
        return  $endpoint->getScheme(). '://' . $endpoint->getHost() . ':' . $endpoint->getPort() . $endpoint->getPath() . '/' . $endpoint->getCore() . '/';
126
    }
127
128
    /**
129
     * @return Endpoint|null
130
     */
131 58
    public function getPrimaryEndpoint()
132
    {
133 58
        return is_array($this->client->getEndpoints()) ? reset($this->client->getEndpoints()) : null;
0 ignored issues
show
introduced by
The condition is_array($this->client->getEndpoints()) is always true.
Loading history...
134
    }
135
136
    /**
137
     * Central method for making a get operation against this Solr Server
138
     *
139
     * @param string $url
140
     * @return ResponseAdapter
141
     */
142 13
    protected function _sendRawGet($url)
143
    {
144 13
        return $this->_sendRawRequest($url, Request::METHOD_GET);
145
    }
146
147
    /**
148
     * Central method for making a HTTP DELETE operation against the Solr server
149
     *
150
     * @param string $url
151
     * @return ResponseAdapter
152
     */
153 4
    protected function _sendRawDelete($url)
154
    {
155 4
        return $this->_sendRawRequest($url, Request::METHOD_DELETE);
156
    }
157
158
    /**
159
     * Central method for making a post operation against this Solr Server
160
     *
161
     * @param string $url
162
     * @param string $rawPost
163
     * @param string $contentType
164
     * @return ResponseAdapter
165
     */
166 4
    protected function _sendRawPost($url, $rawPost, $contentType = 'text/xml; charset=UTF-8')
167
    {
168
        $initializeRequest = function(Request $request) use ($rawPost, $contentType) {
169 4
            $request->setRawData($rawPost);
170 4
            $request->addHeader('Content-Type: ' . $contentType);
171 4
            return $request;
172 4
        };
173
174 4
        return $this->_sendRawRequest($url, Request::METHOD_POST, $rawPost, $initializeRequest);
175
    }
176
177
    /**
178
     * Method that performs an http request with the solarium client.
179
     *
180
     * @param string $url
181
     * @param string $method
182
     * @param string $body
183
     * @param ?\Closure $initializeRequest
184
     * @return ResponseAdapter
185
     */
186 13
    protected function _sendRawRequest(
187
        string $url,
188
        $method = Request::METHOD_GET,
189
        $body = '',
190
        \Closure $initializeRequest = null
191
    ) {
192 13
        $logSeverity = SolrLogManager::INFO;
193 13
        $exception = null;
194 13
        $url = $this->reviseUrl($url);
195
        try {
196 13
            $request = $this->buildSolariumRequestFromUrl($url, $method);
197 13
            if($initializeRequest !== null) {
198 4
                $request = $initializeRequest($request);
199
            }
200 13
            $response = $this->executeRequest($request);
201 1
        } catch (HttpException $exception) {
202 1
            $logSeverity = SolrLogManager::ERROR;
203 1
            $response = new ResponseAdapter($exception->getBody(), $exception->getCode(), $exception->getMessage());
204
        }
205
206 13
        if ($this->configuration->getLoggingQueryRawPost() || $response->getHttpStatus() != 200) {
207 3
            $message = 'Querying Solr using '.$method;
208 3
            $this->writeLog($logSeverity, $message, $url, $response, $exception, $body);
209
        }
210
211 13
        return $response;
212
    }
213
214
    /**
215
     * Revise url
216
     * - Resolve relative paths
217
     *
218
     * @param string $url
219
     * @return string
220
     */
221 13
    protected function reviseUrl(string $url): string
222
    {
223
        /* @var Uri $uri */
224 13
        $uri = GeneralUtility::makeInstance(Uri::class, $url);
225
226 13
        if ((string)$uri->getPath() === '') {
227
            return $url;
228
        }
229
230 13
        $path = trim($uri->getPath(), '/');
231 13
        $pathsCurrent = explode('/', $path);
232 13
        $pathNew = [];
233 13
        foreach ($pathsCurrent as $pathCurrent) {
234 13
            if ($pathCurrent === '..') {
235 6
                array_pop($pathNew);
236 6
                continue;
237
            }
238 13
            if ($pathCurrent === '.') {
239
                continue;
240
            }
241 13
            $pathNew[] = $pathCurrent;
242
        }
243
244 13
        $uri = $uri->withPath(implode('/', $pathNew));
245 13
        return (string)$uri;
246
    }
247
248
    /**
249
     * Build the log data and writes the message to the log
250
     *
251
     * @param integer $logSeverity
252
     * @param string $message
253
     * @param string $url
254
     * @param ResponseAdapter $solrResponse
255
     * @param ?\Exception $exception
256
     * @param string $contentSend
257
     */
258 3
    protected function writeLog($logSeverity, $message, $url, $solrResponse, $exception = null, $contentSend = '')
259
    {
260 3
        $logData = $this->buildLogDataFromResponse($solrResponse, $exception, $url, $contentSend);
261 3
        $this->logger->log($logSeverity, $message, $logData);
262 3
    }
263
264
    /**
265
     * Parses the solr information to build data for the logger.
266
     *
267
     * @param ResponseAdapter $solrResponse
268
     * @param ?\Exception $e
269
     * @param string $url
270
     * @param string $contentSend
271
     * @return array
272
     */
273 3
    protected function buildLogDataFromResponse(ResponseAdapter $solrResponse, \Exception $e = null, $url = '', $contentSend = '')
274
    {
275 3
        $logData = ['query url' => $url, 'response' => (array)$solrResponse];
276
277 3
        if ($contentSend !== '') {
278
            $logData['content'] = $contentSend;
279
        }
280
281 3
        if (!empty($e)) {
282 1
            $logData['exception'] = $e->__toString();
283 1
            return $logData;
284
        } else {
285
            // trigger data parsing
286
            // @extensionScannerIgnoreLine
287 2
            $solrResponse->response;
288 2
            $logData['response data'] = print_r($solrResponse, true);
289 2
            return $logData;
290
        }
291
    }
292
293
    /**
294
     * Call the /admin/ping servlet, can be used to quickly tell if a connection to the
295
     * server is available.
296
     *
297
     * Simply overrides the SolrPhpClient implementation, changing ping from a
298
     * HEAD to a GET request, see http://forge.typo3.org/issues/44167
299
     *
300
     * Also does not report the time, see https://forge.typo3.org/issues/64551
301
     *
302
     * @param boolean $useCache indicates if the ping result should be cached in the instance or not
303
     * @return bool TRUE if Solr can be reached, FALSE if not
304
     */
305 33
    public function ping($useCache = true)
306
    {
307
        try {
308 33
            $httpResponse = $this->performPingRequest($useCache);
309
        } catch (HttpException $exception) {
310
            return false;
311
        }
312
313 33
        return ($httpResponse->getHttpStatus() === 200);
314
    }
315
316
    /**
317
     * Call the /admin/ping servlet, can be used to get the runtime of a ping request.
318
     *
319
     * @param boolean $useCache indicates if the ping result should be cached in the instance or not
320
     * @return double runtime in milliseconds
321
     * @throws \ApacheSolrForTypo3\Solr\PingFailedException
322
     */
323 3
    public function getPingRoundTripRuntime($useCache = true)
324
    {
325
        try {
326 3
            $start = $this->getMilliseconds();
327 3
            $httpResponse = $this->performPingRequest($useCache);
328 2
            $end = $this->getMilliseconds();
329 1
        } catch (HttpException $e) {
330 1
            $message = 'Solr ping failed with unexpected response code: ' . $e->getCode();
331
            /** @var $exception \ApacheSolrForTypo3\Solr\PingFailedException */
332 1
            $exception = GeneralUtility::makeInstance(PingFailedException::class, /** @scrutinizer ignore-type */ $message);
333 1
            throw $exception;
334
        }
335
336 2
        if ($httpResponse->getHttpStatus() !== 200) {
337
            $message = 'Solr ping failed with unexpected response code: ' . $httpResponse->getHttpStatus();
338
            /** @var $exception \ApacheSolrForTypo3\Solr\PingFailedException */
339
            $exception = GeneralUtility::makeInstance(PingFailedException::class, /** @scrutinizer ignore-type */ $message);
340
            throw $exception;
341
        }
342
343 2
        return $end - $start;
344
    }
345
346
    /**
347
     * Performs a ping request and returns the result.
348
     *
349
     * @param boolean $useCache indicates if the ping result should be cached in the instance or not
350
     * @return ResponseAdapter
351
     */
352 36
    protected function performPingRequest($useCache = true)
353
    {
354 36
        $cacheKey = (string)($this);
355 36
        if ($useCache && isset(static::$pingCache[$cacheKey])) {
356 31
            return static::$pingCache[$cacheKey];
357
        }
358
359 36
        $pingQuery = $this->client->createPing();
360 36
        $pingResult = $this->createAndExecuteRequest($pingQuery);
361
362 35
        if ($useCache) {
363 34
            static::$pingCache[$cacheKey] = $pingResult;
364
        }
365
366 35
        return $pingResult;
367
    }
368
369
    /**
370
     * Returns the current time in milliseconds.
371
     *
372
     * @return double
373
     */
374 3
    protected function getMilliseconds()
375
    {
376 3
        return GeneralUtility::milliseconds();
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Core\Utility\G...Utility::milliseconds() has been deprecated: will be removed in TYPO3 v11.0. Use the native PHP functions round(microtime(true) * 1000) instead. ( Ignorable by Annotation )

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

376
        return /** @scrutinizer ignore-deprecated */ GeneralUtility::milliseconds();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
377
    }
378
379
    /**
380
     * @param QueryInterface $query
381
     * @return ResponseAdapter
382
     */
383 62
    protected function createAndExecuteRequest(QueryInterface $query): ResponseAdapter
384
    {
385 62
        $request = $this->client->createRequest($query);
386 62
        return $this->executeRequest($request);
387
    }
388
389
    /**
390
     * @param $request
391
     * @return ResponseAdapter
392
     */
393 78
    protected function executeRequest($request): ResponseAdapter
394
    {
395 78
        $result = $this->client->executeRequest($request);
396 74
        return new ResponseAdapter($result->getBody(), $result->getStatusCode(), $result->getStatusMessage());
397
    }
398
399
    /**
400
     * Build the request for Solarium.
401
     *
402
     * Important: The endpoint already contains the API information.
403
     * The internal Solarium will append the information including the core if set.
404
     *
405
     * @param string $url
406
     * @param string $httpMethod
407
     * @return Request
408
     */
409 13
    protected function buildSolariumRequestFromUrl(string $url, $httpMethod = Request::METHOD_GET): Request
410
    {
411 13
        $params = [];
412 13
        parse_str(parse_url($url, PHP_URL_QUERY), $params);
413 13
        $request = new Request();
414 13
        $path = parse_url($url, PHP_URL_PATH);
415 13
        $endpoint = $this->getPrimaryEndpoint();
416 13
        $api = $request->getApi() === Request::API_V1 ? 'solr' : 'api';
417 13
        $coreBasePath = $endpoint->getPath() . '/' . $api . '/' . $endpoint->getCore() . '/';
418
419 13
        $handler = $this->buildRelativePath($coreBasePath, $path);
420 13
        $request->setMethod($httpMethod);
421 13
        $request->setParams($params);
422 13
        $request->setHandler($handler);
423 13
        return $request;
424
    }
425
426
    /**
427
     * Build a relative path from base path to target path.
428
     * Required since Solarium contains the core information
429
     *
430
     * @param string $basePath
431
     * @param string $targetPath
432
     * @return string
433
     */
434 13
    protected function buildRelativePath(string $basePath, string $targetPath): string
435
    {
436 13
        $basePath = trim($basePath, '/');
437 13
        $targetPath = trim($targetPath, '/');
438 13
        $baseElements = explode('/', $basePath);
439 13
        $targetElements = explode('/', $targetPath);
440 13
        $targetSegment = array_pop($targetElements);
441 13
        foreach ($baseElements as $i => $segment) {
442 13
            if (isset($targetElements[$i]) && $segment === $targetElements[$i]) {
443 13
                unset($baseElements[$i], $targetElements[$i]);
444
            } else {
445 6
                break;
446
            }
447
        }
448 13
        $targetElements[] = $targetSegment;
449 13
        return str_repeat('../', count($baseElements)) . implode('/', $targetElements);
450
    }
451
}
452