Passed
Push — master ( 5adbd6...5901ba )
by Timo
20:55
created

Search::getResultDocumentsEscaped()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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