Passed
Push — master ( 48f77b...329461 )
by Timo
15:28
created

Search::ping()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 7.1753

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 5
cts 12
cp 0.4167
rs 8.6845
c 0
b 0
f 0
cc 4
eloc 13
nc 5
nop 1
crap 7.1753
1
<?php
2
namespace ApacheSolrForTypo3\Solr;
3
4
/***************************************************************
5
 *  Copyright notice
6
 *
7
 *  (c) 2009-2015 Ingo Renner <[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 2 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\Query\Modifier\Modifier;
28
use ApacheSolrForTypo3\Solr\Search\ResponseModifier;
29
use ApacheSolrForTypo3\Solr\Search\FacetsModifier;
30
use ApacheSolrForTypo3\Solr\Search\SearchAware;
31
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
32
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
33
use TYPO3\CMS\Core\SingletonInterface;
34
use TYPO3\CMS\Core\Utility\GeneralUtility;
35
36
/**
37
 * Class to handle solr search requests
38
 *
39
 * @author Ingo Renner <[email protected]>
40
 */
41
class Search implements SingletonInterface
42
{
43
44
    /**
45
     * An instance of the Solr service
46
     *
47
     * @var SolrService
48
     */
49
    protected $solr = null;
50
51
    /**
52
     * The search query
53
     *
54
     * @var Query
55
     */
56
    protected $query = null;
57
58
    /**
59
     * The search response
60
     *
61
     * @var \Apache_Solr_Response
62
     */
63
    protected $response = null;
64
65
    /**
66
     * Flag for marking a search
67
     *
68
     * @var bool
69
     */
70
    protected $hasSearched = false;
71
72
    /**
73
     * @var TypoScriptConfiguration
74
     */
75
    protected $configuration;
76
77
    // TODO Override __clone to reset $response and $hasSearched
78
79
    /**
80
     * @var \ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager
81
     */
82
    protected $logger = null;
83
84
    /**
85
     * Constructor
86
     *
87
     * @param SolrService $solrConnection The Solr connection to use for searching
88
     */
89 49
    public function __construct(SolrService $solrConnection = null)
90
    {
91 49
        $this->logger = GeneralUtility::makeInstance(SolrLogManager::class, __CLASS__);
92
93 49
        $this->solr = $solrConnection;
94
95 49
        if (is_null($solrConnection)) {
96
            /** @var $connectionManager ConnectionManager */
97 1
            $connectionManager = GeneralUtility::makeInstance(ConnectionManager::class);
98 1
            $this->solr = $connectionManager->getConnectionByPageId($GLOBALS['TSFE']->id, $GLOBALS['TSFE']->sys_language_uid);
99
        }
100
101 49
        $this->configuration = Util::getSolrConfiguration();
102 49
    }
103
104
    /**
105
     * Gets the Solr connection used by this search.
106
     *
107
     * @return SolrService Solr connection
108
     */
109
    public function getSolrConnection()
110
    {
111
        return $this->solr;
112
    }
113
114
    /**
115
     * Sets the Solr connection used by this search.
116
     *
117
     * Since ApacheSolrForTypo3\Solr\Search is a \TYPO3\CMS\Core\SingletonInterface, this is needed to
118
     * be able to switch between multiple cores/connections during
119
     * one request
120
     *
121
     * @param SolrService $solrConnection
122
     */
123
    public function setSolrConnection(SolrService $solrConnection)
124
    {
125
        $this->solr = $solrConnection;
126
    }
127
128
    /**
129
     * Executes a query against a Solr server.
130
     *
131
     * 1) Gets the query string
132
     * 2) Conducts the actual search
133
     * 3) Checks debug settings
134
     *
135
     * @param Query $query The query with keywords, filters, and so on.
136
     * @param int $offset Result offset for pagination.
137
     * @param int $limit Maximum number of results to return. If set to NULL, this value is taken from the query object.
138
     * @return \Apache_Solr_Response Solr response
139
     */
140 26
    public function search(Query $query, $offset = 0, $limit = 10)
141
    {
142 26
        $query = $this->modifyQuery($query);
143 26
        $this->query = $query;
144
145 26
        if (empty($limit)) {
146 24
            $limit = $query->getResultsPerPage();
147
        }
148
149
        try {
150 26
            $response = $this->solr->search(
151 26
                $query->getQueryString(),
152
                $offset,
153
                $limit,
154 26
                $query->getQueryParameters()
155
            );
156
157 25
            if ($this->configuration->getLoggingQueryQueryString()) {
158
                $this->logger->log(
159
                    SolrLogManager::INFO,
160
                    'Querying Solr, getting result',
161
                    [
162
                        'query string' => $query->getQueryString(),
163
                        'query parameters' => $query->getQueryParameters(),
164
                        'response' => json_decode($response->getRawResponse(),
165 25
                            true)
166
                    ]
167
                );
168
            }
169 1
        } catch (\RuntimeException $e) {
170 1
            $response = $this->solr->getResponse();
171
172 1
            if ($this->configuration->getLoggingExceptions()) {
173 1
                $this->logger->log(
174 1
                    SolrLogManager::ERROR,
175 1
                    'Exception while querying Solr',
176
                    [
177 1
                        'exception' => $e->__toString(),
178 1
                        'query' => (array)$query,
179 1
                        'offset' => $offset,
180 1
                        'limit' => $limit
181
                    ]
182
                );
183
            }
184
        }
185
186 26
        $response = $this->modifyResponse($response);
187 26
        $this->response = $response;
188 26
        $this->hasSearched = true;
189
190 26
        return $this->response;
191
    }
192
193
    /**
194
     * Allows to modify a query before eventually handing it over to Solr.
195
     *
196
     * @param Query $query The current query before it's being handed over to Solr.
197
     * @return Query The modified query that is actually going to be given to Solr.
198
     */
199 26
    protected function modifyQuery(Query $query)
200
    {
201
        // hook to modify the search query
202 26
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'])) {
203 24
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchQuery'] as $classReference) {
204 24
                $queryModifier = GeneralUtility::getUserObj($classReference);
205
206 24
                if ($queryModifier instanceof Modifier) {
207 24
                    if ($queryModifier instanceof SearchAware) {
208
                        $queryModifier->setSearch($this);
209
                    }
210
211 24
                    $query = $queryModifier->modifyQuery($query);
212
                } else {
213
                    throw new \UnexpectedValueException(
214
                        get_class($queryModifier) . ' must implement interface ' . Modifier::class,
215 24
                        1310387414
216
                    );
217
                }
218
            }
219
        }
220
221 26
        return $query;
222
    }
223
224
    /**
225
     * Allows to modify a response returned from Solr before returning it to
226
     * the rest of the extension.
227
     *
228
     * @param \Apache_Solr_Response $response The response as returned by Solr
229
     * @return \Apache_Solr_Response The modified response that is actually going to be returned to the extension.
230
     * @throws \UnexpectedValueException if a response modifier does not implement interface ApacheSolrForTypo3\Solr\Search\ResponseModifier
231
     */
232 26
    protected function modifyResponse(\Apache_Solr_Response $response)
233
    {
234
        // hook to modify the search response
235 26
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchResponse'])) {
236
            foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifySearchResponse'] as $classReference) {
237
                $responseModifier = GeneralUtility::getUserObj($classReference);
238
239
                if ($responseModifier instanceof ResponseModifier) {
240
                    if ($responseModifier instanceof SearchAware) {
241
                        $responseModifier->setSearch($this);
242
                    }
243
244
                    $response = $responseModifier->modifyResponse($response);
245
                } else {
246
                    throw new \UnexpectedValueException(
247
                        get_class($responseModifier) . ' must implement interface ' . ResponseModifier::class,
248
                        1343147211
249
                    );
250
                }
251
            }
252
253
            // add modification indicator
254
            $response->response->isModified = true;
255
        }
256
257 26
        return $response;
258
    }
259
260
    /**
261
     * Sends a ping to the solr server to see whether it is available.
262
     *
263
     * @param bool $useCache Set to true if the cache should be used.
264
     * @return bool Returns TRUE on successful ping.
265
     * @throws \Exception Throws an exception in case ping was not successful.
266
     */
267 25
    public function ping($useCache = true)
268
    {
269 25
        $solrAvailable = false;
270
271
        try {
272 25
            if (!$this->solr->ping(2, $useCache)) {
273
                throw new \Exception('Solr Server not responding.', 1237475791);
274
            }
275
276 25
            $solrAvailable = true;
277
        } catch (\Exception $e) {
278
            if ($this->configuration->getLoggingExceptions()) {
279
                $this->logger->log(
280
                    SolrLogManager::ERROR,
281
                    'Exception while trying to ping the solr server',
282
                    [
283
                        $e->__toString()
284
                    ]
285
                );
286
            }
287
        }
288
289 25
        return $solrAvailable;
290
    }
291
292
    /**
293
     * checks whether a search has been executed
294
     *
295
     * @return bool    TRUE if there was a search, FALSE otherwise (if the user just visited the search page f.e.)
296
     */
297 25
    public function hasSearched()
298
    {
299 25
        return $this->hasSearched;
300
    }
301
302
    /**
303
     * Gets the query object.
304
     *
305
     * @return Query Query
306
     */
307 28
    public function getQuery()
308
    {
309 28
        return $this->query;
310
    }
311
312
    /**
313
     * Gets the Solr response
314
     *
315
     * @return \Apache_Solr_Response
316
     */
317 18
    public function getResponse()
318
    {
319 18
        return $this->response;
320
    }
321
322
    public function getRawResponse()
323
    {
324
        return $this->response->getRawResponse();
325
    }
326
327 18
    public function getResponseHeader()
328
    {
329 18
        return $this->getResponse()->responseHeader;
330
    }
331
332 18
    public function getResponseBody()
333
    {
334 18
        return $this->getResponse()->response;
335
    }
336
337
    /**
338
     * Returns all results documents raw. Use with caution!
339
     *
340
     * @return \Apache_Solr_Document[]
341
     */
342
    public function getResultDocumentsRaw()
343
    {
344
        return $this->getResponseBody()->docs;
345
    }
346
347
    /**
348
     * Returns all result documents but applies htmlspecialchars() on all fields retrieved
349
     * from solr except the configured fields in plugin.tx_solr.search.trustedFields
350
     *
351
     * @return \Apache_Solr_Document[]
352
     */
353 18
    public function getResultDocumentsEscaped()
354
    {
355 18
        return $this->applyHtmlSpecialCharsOnAllFields($this->getResponseBody()->docs);
356
    }
357
358
    /**
359
     * This method is used to apply htmlspecialchars on all document fields that
360
     * are not configured to be secure. Secure mean that we know where the content is coming from.
361
     *
362
     * @param array $documents
363
     * @return \Apache_Solr_Document[]
364
     */
365 18
    protected function applyHtmlSpecialCharsOnAllFields(array $documents)
366
    {
367 18
        $trustedSolrFields = $this->configuration->getSearchTrustedFieldsArray();
368
369 18
        foreach ($documents as $key => $document) {
370 18
            $fieldNames = $document->getFieldNames();
371
372 18
            foreach ($fieldNames as $fieldName) {
373 18
                if (in_array($fieldName, $trustedSolrFields)) {
374
                    // we skip this field, since it was marked as secure
375 18
                    continue;
376
                }
377
378 18
                $document->{$fieldName} = $this->applyHtmlSpecialCharsOnSingleFieldValue($document->{$fieldName});
379
            }
380
381 18
            $documents[$key] = $document;
382
        }
383
384 18
        return $documents;
385
    }
386
387
    /**
388
     * Applies htmlspecialchars on all items of an array of a single value.
389
     *
390
     * @param $fieldValue
391
     * @return array|string
392
     */
393 18
    protected function applyHtmlSpecialCharsOnSingleFieldValue($fieldValue)
394
    {
395 18
        if (is_array($fieldValue)) {
396 17
            foreach ($fieldValue as $key => $fieldValueItem) {
397 17
                $fieldValue[$key] = htmlspecialchars($fieldValueItem, null, null, false);
398
            }
399
        } else {
400 18
            $fieldValue = htmlspecialchars($fieldValue, null, null, false);
401
        }
402
403 18
        return $fieldValue;
404
    }
405
406
    /**
407
     * Gets the time Solr took to execute the query and return the result.
408
     *
409
     * @return int Query time in milliseconds
410
     */
411 18
    public function getQueryTime()
412
    {
413 18
        return $this->getResponseHeader()->QTime;
414
    }
415
416
    /**
417
     * Gets the number of results per page.
418
     *
419
     * @return int Number of results per page
420
     */
421
    public function getResultsPerPage()
422
    {
423
        return $this->getResponseHeader()->params->rows;
424
    }
425
426
    /**
427
     * Gets all facets with their fields, options, and counts.
428
     *
429
     * @return array
430
     */
431 18
    public function getFacetCounts()
432
    {
433 18
        static $facetCountsModified = false;
434 18
        static $facetCounts = null;
435
436 18
        $unmodifiedFacetCounts = $this->response->facet_counts;
437
438 18
        if (is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifyFacets'])) {
439
            if (!$facetCountsModified) {
440
                $facetCounts = $unmodifiedFacetCounts;
441
442
                foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['solr']['modifyFacets'] as $classReference) {
443
                    $facetsModifier = GeneralUtility::getUserObj($classReference);
444
445
                    if ($facetsModifier instanceof FacetsModifier) {
446
                        $facetCounts = $facetsModifier->modifyFacets($facetCounts);
447
                        $facetCountsModified = true;
448
                    } else {
449
                        throw new \UnexpectedValueException(
450
                            get_class($facetsModifier) . ' must implement interface ' . FacetsModifier::class,
451
                            1310387526
452
                        );
453
                    }
454
                }
455
            }
456
        } else {
457 18
            $facetCounts = $unmodifiedFacetCounts;
458
        }
459
460 18
        return $facetCounts;
461
    }
462
463 18
    public function getFacetFieldOptions($facetField)
464
    {
465 18
        $facetOptions = null;
466
467 18
        if (property_exists($this->getFacetCounts()->facet_fields,
468
            $facetField)) {
469 18
            $facetOptions = get_object_vars($this->getFacetCounts()->facet_fields->$facetField);
470
        }
471
472 18
        return $facetOptions;
473
    }
474
475
    public function getFacetQueryOptions($facetField)
476
    {
477
        $options = [];
478
479
        $facetQueries = get_object_vars($this->getFacetCounts()->facet_queries);
480
        foreach ($facetQueries as $facetQuery => $numberOfResults) {
481
            // remove tags from the facet.query response, for facet.field
482
            // and facet.range Solr does that on its own automatically
483
            $facetQuery = preg_replace('/^\{!ex=[^\}]*\}(.*)/', '\\1',
484
                $facetQuery);
485
486
            if (GeneralUtility::isFirstPartOfStr($facetQuery, $facetField)) {
487
                $options[$facetQuery] = $numberOfResults;
488
            }
489
        }
490
491
        // filter out queries with no results
492
        $options = array_filter($options);
493
494
        return $options;
495
    }
496
497
    public function getFacetRangeOptions($rangeFacetField)
498
    {
499
        return get_object_vars($this->getFacetCounts()->facet_ranges->$rangeFacetField);
500
    }
501
502 20
    public function getNumberOfResults()
503
    {
504 20
        return $this->response->response->numFound;
505
    }
506
507
    /**
508
     * Gets the result offset.
509
     *
510
     * @return int Result offset
511
     */
512 18
    public function getResultOffset()
513
    {
514 18
        return $this->response->response->start;
515
    }
516
517 27
    public function getMaximumResultScore()
518
    {
519 27
        return $this->response->response->maxScore;
520
    }
521
522
    public function getDebugResponse()
523
    {
524
        return $this->response->debug;
525
    }
526
527 18
    public function getHighlightedContent()
528
    {
529 18
        $highlightedContent = false;
530
531 18
        if ($this->response->highlighting) {
532 18
            $highlightedContent = $this->response->highlighting;
533
        }
534
535 18
        return $highlightedContent;
536
    }
537
538 20
    public function getSpellcheckingSuggestions()
539
    {
540 20
        $spellcheckingSuggestions = false;
541
542 20
        $suggestions = (array)$this->response->spellcheck->suggestions;
543
544 20
        if (!empty($suggestions)) {
545 5
            $spellcheckingSuggestions = $suggestions;
546
547 5
            if (isset($this->response->spellcheck->collations)) {
548 5
                $collections = (array) $this->response->spellcheck->collations;
549 5
                $spellcheckingSuggestions['collation'] = $collections['collation'];
550
            }
551
        }
552
553 20
        return $spellcheckingSuggestions;
554
    }
555
}
556