Passed
Push — master ( 791909...fe60d3 )
by Timo
52s
created

Search::getFacetFieldOptions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 6
nc 2
nop 1
crap 2
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 27
    public function search(Query $query, $offset = 0, $limit = 10)
141
    {
142 27
        $query = $this->modifyQuery($query);
143 27
        $this->query = $query;
144
145 27
        if (empty($limit)) {
146 24
            $limit = $query->getResultsPerPage();
147
        }
148
149
        try {
150 27
            $response = $this->solr->search(
151 27
                $query->getQueryString(),
152
                $offset,
153
                $limit,
154 27
                $query->getQueryParameters()
155
            );
156
157 27
            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
                            true)
166
                    ]
167
                );
168
            }
169
        } catch (\RuntimeException $e) {
170
            $response = $this->solr->getResponse();
171
172
            if ($this->configuration->getLoggingExceptions()) {
173
                $this->logger->log(
174
                    SolrLogManager::ERROR,
175
                    'Exception while querying Solr',
176
                    [
177
                        'exception' => $e->__toString(),
178
                        'query' => (array)$query,
179
                        'offset' => $offset,
180
                        'limit' => $limit
181
                    ]
182
                );
183
            }
184
        }
185
186 27
        $response = $this->modifyResponse($response);
187 27
        $this->response = $response;
188 27
        $this->hasSearched = true;
189
190 27
        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 27
    protected function modifyQuery(Query $query)
200
    {
201
        // hook to modify the search query
202 27
        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
                        1310387414
216
                    );
217
                }
218
            }
219
        }
220
221 27
        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 27
    protected function modifyResponse(\Apache_Solr_Response $response)
233
    {
234
        // hook to modify the search response
235 27
        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;
0 ignored issues
show
Bug introduced by
The property response does not seem to exist. Did you mean _response?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
255
        }
256
257 27
        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 19
    public function getResponse()
318
    {
319 19
        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 19
    public function getResponseBody()
333
    {
334 19
        return $this->getResponse()->response;
0 ignored issues
show
Bug introduced by
The property response does not seem to exist. Did you mean _response?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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 19
    public function getResultDocumentsEscaped()
354
    {
355 19
        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 19
    protected function applyHtmlSpecialCharsOnAllFields(array $documents)
366
    {
367 19
        $trustedSolrFields = $this->configuration->getSearchTrustedFieldsArray();
368
369 19
        foreach ($documents as $key => $document) {
370 19
            $fieldNames = $document->getFieldNames();
371
372 19
            foreach ($fieldNames as $fieldName) {
373 19
                if (in_array($fieldName, $trustedSolrFields)) {
374
                    // we skip this field, since it was marked as secure
375 19
                    continue;
376
                }
377
378 19
                $document->{$fieldName} = $this->applyHtmlSpecialCharsOnSingleFieldValue($document->{$fieldName});
379
            }
380
381 19
            $documents[$key] = $document;
382
        }
383
384 19
        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 19
    protected function applyHtmlSpecialCharsOnSingleFieldValue($fieldValue)
394
    {
395 19
        if (is_array($fieldValue)) {
396 18
            foreach ($fieldValue as $key => $fieldValueItem) {
397 18
                $fieldValue[$key] = htmlspecialchars($fieldValueItem, null, null, false);
398
            }
399
        } else {
400 19
            $fieldValue = htmlspecialchars($fieldValue, null, null, false);
401
        }
402
403 19
        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;
0 ignored issues
show
Bug introduced by
The property facet_counts does not seem to exist in Apache_Solr_Response.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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 18
            $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;
0 ignored issues
show
Bug introduced by
The property response does not seem to exist. Did you mean _response?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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;
0 ignored issues
show
Bug introduced by
The property response does not seem to exist. Did you mean _response?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
515
    }
516
517 27
    public function getMaximumResultScore()
518
    {
519 27
        return $this->response->response->maxScore;
0 ignored issues
show
Bug introduced by
The property response does not seem to exist. Did you mean _response?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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;
0 ignored issues
show
Bug introduced by
The property highlighting does not seem to exist in Apache_Solr_Response.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
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