Passed
Pull Request — master (#1384)
by Timo
19:49
created

Query::setResultsPerPage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
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\Domain\Site\SiteHashService;
28
use ApacheSolrForTypo3\Solr\FieldProcessor\PageUidToHierarchy;
29
use ApacheSolrForTypo3\Solr\System\Configuration\TypoScriptConfiguration;
30
use ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager;
31
use TYPO3\CMS\Core\Utility\GeneralUtility;
32
33
/**
34
 * A Solr search query
35
 *
36
 * @author Ingo Renner <[email protected]>
37
 */
38
class Query
39
{
40
41
    // FIXME extract link building from the query, it's not the query's domain
42
43
    const SORT_ASC = 'ASC';
44
    const SORT_DESC = 'DESC';
45
46
    const OPERATOR_AND = 'AND';
47
    const OPERATOR_OR = 'OR';
48
49
    /**
50
     * Used to identify the queries.
51
     *
52
     * @var int
53
     */
54
    protected static $idCount = 0;
55
56
    /**
57
     * @var int
58
     */
59
    protected $id;
60
61
    /**
62
     * @var TypoScriptConfiguration
63
     */
64
    protected $solrConfiguration;
65
66
    /**
67
     * @var string
68
     */
69
    protected $keywords;
70
71
    /**
72
     * @var string
73
     */
74
    protected $keywordsRaw;
75
76
    /**
77
     * @var array
78
     */
79
    protected $filters = [];
80
81
    /**
82
     * @var string
83
     */
84
    protected $sorting;
85
86
    // TODO check usage of these two variants, especially the check for $rawQueryString in getQueryString()
87
    /**
88
     * @var
89
     */
90
    protected $queryString;
91
92
    /**
93
     * @var array
94
     */
95
    protected $queryParameters = [];
96
97
    /**
98
     * @var int
99
     */
100
    protected $resultsPerPage;
101
102
    /**
103
     * @var int
104
     */
105
    protected $page;
106
107
    /**
108
     * @var int
109
     */
110
    protected $linkTargetPageId;
111
112
    /**
113
     * Holds the query fields with their associated boosts. The key represents
114
     * the field name, value represents the field's boost. These are the fields
115
     * that will actually be searched.
116
     *
117
     * Used in Solr's qf parameter
118
     *
119
     * @var array
120
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#qf_.28Query_Fields.29
121
     */
122
    protected $queryFields = [];
123
124
    /**
125
     * List of fields that will be returned in the result documents.
126
     *
127
     * used in Solr's fl parameter
128
     *
129
     * @var array
130
     * @see http://wiki.apache.org/solr/CommonQueryParameters#fl
131
     */
132
    protected $fieldList = [];
133
134
    /**
135
     * @var array
136
     */
137
    protected $filterFields;
138
139
    /**
140
     * @var bool
141
     */
142
    private $rawQueryString = false;
143
144
    /**
145
     * The field by which the result will be collapsed
146
     * @var string
147
     */
148
    protected $variantField = 'variantId';
149
150
    /**
151
     * @var SiteHashService
152
     */
153
    protected $siteHashService = null;
154
155
    /**
156
     * @var \ApacheSolrForTypo3\Solr\System\Logging\SolrLogManager
157
     */
158
    protected $logger = null;
159
160
    /**
161
     * Query constructor.
162
     * @param string $keywords
163
     * @param TypoScriptConfiguration $solrConfiguration
164
     * @param SiteHashService|null $siteHashService
165
     */
166 125
    public function __construct($keywords, $solrConfiguration = null, SiteHashService $siteHashService = null)
167
    {
168 125
        $keywords = (string)$keywords;
169
170 125
        $this->logger = GeneralUtility::makeInstance(SolrLogManager::class, __CLASS__);
171 125
        $this->solrConfiguration = is_null($solrConfiguration) ? Util::getSolrConfiguration() : $solrConfiguration;
172 125
        $this->siteHashService = is_null($siteHashService) ? GeneralUtility::makeInstance(SiteHashService::class) : $siteHashService;
173
174 125
        $this->setKeywords($keywords);
175 125
        $this->sorting = '';
176
177
        // What fields to search
178 125
        $queryFields = $this->solrConfiguration->getSearchQueryQueryFields();
179 125
        if ($queryFields != '') {
180 30
            $this->setQueryFieldsFromString($queryFields);
181
        }
182
183
        // What fields to return from Solr
184 125
        $this->fieldList = $this->solrConfiguration->getSearchQueryReturnFieldsAsArray(['*', 'score']);
185 125
        $this->linkTargetPageId = $this->solrConfiguration->getSearchTargetPage();
186
187 125
        $this->initializeQuery();
188
189 125
        $this->id = ++self::$idCount;
190 125
    }
191
192
    /**
193
     * @return void
194
     */
195 124
    protected function initializeQuery()
196
    {
197 124
        $this->initializeCollapsingFromConfiguration();
198 124
    }
199
200
    /**
201
     * Takes a string of comma separated query fields and _overwrites_ the
202
     * currently set query fields. Boost can also be specified in through the
203
     * given string.
204
     *
205
     * Example: "title^5, subtitle^2, content, author^0.5"
206
     * This sets the query fields to title with  a boost of 5.0, subtitle with
207
     * a boost of 2.0, content with a default boost of 1.0 and the author field
208
     * with a boost of 0.5
209
     *
210
     * @param string $queryFields A string defining which fields to query and their associated boosts
211
     * @return void
212
     */
213 31
    public function setQueryFieldsFromString($queryFields)
214
    {
215 31
        $fields = GeneralUtility::trimExplode(',', $queryFields, true);
216
217 31
        foreach ($fields as $field) {
218 31
            $fieldNameAndBoost = explode('^', $field);
219
220 31
            $boost = 1.0;
221 31
            if (isset($fieldNameAndBoost[1])) {
222 31
                $boost = floatval($fieldNameAndBoost[1]);
223
            }
224
225 31
            $this->setQueryField($fieldNameAndBoost[0], $boost);
226
        }
227 31
    }
228
229
    /**
230
     * Sets a query field and its boost. If the field does not exist yet, it
231
     * gets added. Boost is optional, if left out a default boost of 1.0 is
232
     * applied.
233
     *
234
     * @param string $fieldName The field's name
235
     * @param float $boost Optional field boost, defaults to 1.0
236
     * @return void
237
     */
238 31
    public function setQueryField($fieldName, $boost = 1.0)
239
    {
240 31
        $this->queryFields[$fieldName] = (float)$boost;
241 31
    }
242
243
    /**
244
     * magic implementation for clone(), makes sure that the id counter is
245
     * incremented
246
     *
247
     * @return void
248
     */
249
    public function __clone()
250
    {
251
        $this->id = ++self::$idCount;
252
    }
253
254
    /**
255
     * returns a string representation of the query
256
     *
257
     * @return string the string representation of the query
258
     */
259 8
    public function __toString()
260
    {
261 8
        return $this->getQueryString();
262
    }
263
264
    /**
265
     * Builds the query string which is then used for Solr's q parameters
266
     *
267
     * @return string Solr query string
268
     */
269 41
    public function getQueryString()
270
    {
271 41
        if (!$this->rawQueryString) {
272 38
            $this->buildQueryString();
273
        }
274
275 41
        return $this->queryString;
276
    }
277
278
    /**
279
     * Sets the query string without any escaping.
280
     *
281
     * Be cautious with this function!
282
     * TODO remove this method as it basically just sets the q parameter / keywords
283
     *
284
     * @param string $queryString The raw query string.
285
     */
286 4
    public function setQueryString($queryString)
287
    {
288 4
        $this->queryString = $queryString;
289 4
    }
290
291
    /**
292
     * Creates the string that is later used as the q parameter in the solr query
293
     *
294
     * @return void
295
     */
296 38
    protected function buildQueryString()
297
    {
298
        // very simple for now
299 38
        $this->queryString = $this->keywords;
300 38
    }
301
302
    /**
303
     * Sets whether a raw query sting should be used, that is, whether the query
304
     * string should be escaped or not.
305
     *
306
     * @param bool $useRawQueryString TRUE to use raw queries (like Lucene Query Language) or FALSE for regular, escaped queries
307
     */
308 4
    public function useRawQueryString($useRawQueryString)
309
    {
310 4
        $this->rawQueryString = (boolean)$useRawQueryString;
311 4
    }
312
313
    /**
314
     * Returns the query's ID.
315
     *
316
     * @return int The query's ID.
317
     */
318
    public function getId()
319
    {
320
        return $this->id;
321
    }
322
323
    /**
324
     * Quote and escape search strings
325
     *
326
     * @param string $string String to escape
327
     * @return string The escaped/quoted string
328
     */
329 125
    public function escape($string)
330
    {
331
        // when we have a numeric string only, nothing needs to be done
332 125
        if (is_numeric($string)) {
333 1
            return $string;
334
        }
335
336
        // when no whitespaces are in the query we can also just escape the special characters
337 125
        if (preg_match('/\W/', $string) != 1) {
338 93
            return $this->escapeSpecialCharacters($string);
339
        }
340
341
        // when there are no quotes inside the query string we can also just escape the whole string
342 48
        $hasQuotes = strrpos($string, '"') !== false;
343 48
        if (!$hasQuotes) {
344 41
            return $this->escapeSpecialCharacters($string);
345
        }
346
347 7
        $result = $this->tokenizeByQuotesAndEscapeDependingOnContext($string);
348
349 7
        return $result;
350
    }
351
352
    /**
353
     * This method is used to escape the content in the query string surrounded by quotes
354
     * different then when it is not in a quoted context.
355
     *
356
     * @param string $string
357
     * @return string
358
     */
359 7
    protected function tokenizeByQuotesAndEscapeDependingOnContext($string)
360
    {
361 7
        $result = '';
362 7
        $quotesCount = substr_count($string, '"');
363 7
        $isEvenAmountOfQuotes = $quotesCount % 2 === 0;
364
365
        // go over all quote segments and apply escapePhrase inside a quoted
366
        // context and escapeSpecialCharacters outside the quoted context.
367 7
        $segments = explode('"', $string);
368 7
        $segmentsIndex = 0;
369 7
        foreach ($segments as $segment) {
370 7
            $isInQuote = $segmentsIndex % 2 !== 0;
371 7
            $isLastQuote = $segmentsIndex === $quotesCount;
372
373 7
            if ($isLastQuote && !$isEvenAmountOfQuotes) {
374 1
                $result .= '\"';
375
            }
376
377 7
            if ($isInQuote && !$isLastQuote) {
378 6
                $result .= $this->escapePhrase($segment);
379
            } else {
380 7
                $result .= $this->escapeSpecialCharacters($segment);
381
            }
382
383 7
            $segmentsIndex++;
384
        }
385
386 7
        return $result;
387
    }
388
389
    // pagination
390
391
    /**
392
     * Escapes a value meant to be contained in a phrase with characters with
393
     * special meanings in Lucene query syntax.
394
     *
395
     * @param string $value Unescaped - "dirty" - string
396
     * @return string Escaped - "clean" - string
397
     */
398 6
    protected function escapePhrase($value)
399
    {
400 6
        $pattern = '/("|\\\)/';
401 6
        $replace = '\\\$1';
402
403 6
        return '"' . preg_replace($pattern, $replace, $value) . '"';
404
    }
405
406
    /**
407
     * Escapes characters with special meanings in Lucene query syntax.
408
     *
409
     * @param string $value Unescaped - "dirty" - string
410
     * @return string Escaped - "clean" - string
411
     */
412 125
    protected function escapeSpecialCharacters($value)
413
    {
414
        // list taken from http://lucene.apache.org/core/4_4_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description
415
        // which mentions: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
416
        // of which we escape: ( ) { } [ ] ^ " ~ : \ /
417
        // and explicitly don't escape: + - && || ! * ?
418 125
        $pattern = '/(\\(|\\)|\\{|\\}|\\[|\\]|\\^|"|~|\:|\\\\|\\/)/';
419 125
        $replace = '\\\$1';
420
421 125
        return preg_replace($pattern, $replace, $value);
422
    }
423
424
    /**
425
     * Gets the currently showing page's number
426
     *
427
     * @return int page number currently showing
428
     */
429 1
    public function getPage()
430
    {
431 1
        return $this->page;
432
    }
433
434
    /**
435
     * Sets the page that should be shown
436
     *
437
     * @param int $page page number to show
438
     * @return void
439
     */
440 1
    public function setPage($page)
441
    {
442 1
        $this->page = max(intval($page), 0);
443 1
    }
444
445
    /**
446
     * Gets the index of the first result document we're showing
447
     *
448
     * @return int index of the currently first document showing
449
     */
450
    public function getStartIndex()
451
    {
452
        return ($this->page - 1) * $this->resultsPerPage;
453
    }
454
455
    /**
456
     * Gets the index of the last result document we're showing
457
     *
458
     * @return int index of the currently last document showing
459
     */
460
    public function getEndIndex()
461
    {
462
        return $this->page * $this->resultsPerPage;
463
    }
464
465
    // query elevation
466
467
    /**
468
     * Activates and deactivates query elevation for the current query.
469
     *
470
     * @param bool $elevation True to enable query elevation (default), FALSE to disable query elevation.
471
     * @param bool $forceElevation Optionally force elevation so that the elevated documents are always on top regardless of sorting, default to TRUE.
472
     * @param bool $markElevatedResults Mark elevated results
473
     * @return void
474
     */
475 28
    public function setQueryElevation(
476
        $elevation = true,
477
        $forceElevation = true,
478
        $markElevatedResults = true
479
    ) {
480 28
        if ($elevation) {
481 24
            $this->queryParameters['enableElevation'] = 'true';
482 24
            $this->setForceElevation($forceElevation);
483 24
            if ($markElevatedResults) {
484 24
                $this->addReturnField('isElevated:[elevated]');
485
            }
486
        } else {
487 5
            $this->queryParameters['enableElevation'] = 'false';
488 5
            unset($this->queryParameters['forceElevation']);
489 5
            $this->removeReturnField('isElevated:[elevated]');
490 5
            $this->removeReturnField('[elevated]'); // fallback
491
        }
492 28
    }
493
494
    /**
495
     * Enables or disables the forceElevation query parameter.
496
     *
497
     * @param bool $forceElevation
498
     */
499 24
    protected function setForceElevation($forceElevation)
500
    {
501 24
        if ($forceElevation) {
502 23
            $this->queryParameters['forceElevation'] = 'true';
503
        } else {
504 1
            $this->queryParameters['forceElevation'] = 'false';
505
        }
506 24
    }
507
508
    // collapsing
509
510
    /**
511
     * Check whether collapsing is active
512
     *
513
     * @return bool
514
     */
515 3
    public function getIsCollapsing()
516
    {
517 3
        return array_key_exists('collapsing', $this->filters);
518
    }
519
520
    /**
521
     * @param string $fieldName
522
     */
523 3
    public function setVariantField($fieldName)
524
    {
525 3
        $this->variantField = $fieldName;
526 3
    }
527
528
    /**
529
     * @return string
530
     */
531 1
    public function getVariantField()
532
    {
533 1
        return $this->variantField;
534
    }
535
536
    /**
537
     * @param bool $collapsing
538
     */
539 4
    public function setCollapsing($collapsing = true)
540
    {
541 4
        if ($collapsing) {
542 4
            $this->filters['collapsing'] = '{!collapse field=' . $this->variantField . '}';
543 4
            if ($this->solrConfiguration->getSearchVariantsExpand()) {
544 2
                $this->queryParameters['expand'] = 'true';
545 4
                $this->queryParameters['expand.rows'] = $this->solrConfiguration->getSearchVariantsLimit();
546
            }
547
        } else {
548 1
            unset($this->filters['collapsing']);
549 1
            unset($this->queryParameters['expand']);
550 1
            unset($this->queryParameters['expand.rows']);
551
        }
552 4
    }
553
554
    // grouping
555
556
    /**
557
     * Adds a field to the list of fields to return. Also checks whether * is
558
     * set for the fields, if so it's removed from the field list.
559
     *
560
     * @param string $fieldName Name of a field to return in the result documents
561
     */
562 25
    public function addReturnField($fieldName)
563
    {
564 25
        if (strpos($fieldName, '[') === false
565 25
            && strpos($fieldName, ']') === false
566 25
            && in_array('*', $this->fieldList)
567
        ) {
568 1
            $this->fieldList = array_diff($this->fieldList, ['*']);
569
        }
570
571 25
        $this->fieldList[] = $fieldName;
572 25
    }
573
574
    /**
575
     * Removes a field from the list of fields to return (fl parameter).
576
     *
577
     * @param string $fieldName Field to remove from the list of fields to return
578
     */
579 6
    public function removeReturnField($fieldName)
580
    {
581 6
        $key = array_search($fieldName, $this->fieldList);
582
583 6
        if ($key !== false) {
584 2
            unset($this->fieldList[$key]);
585
        }
586 6
    }
587
588
    /**
589
     * Activates and deactivates grouping for the current query.
590
     *
591
     * @param bool $grouping TRUE to enable grouping, FALSE to disable grouping
592
     * @return void
593
     */
594 2
    public function setGrouping($grouping = true)
595
    {
596 2
        if ($grouping) {
597 1
            $this->queryParameters['group'] = 'true';
598 1
            $this->queryParameters['group.format'] = 'grouped';
599 1
            $this->queryParameters['group.ngroups'] = 'true';
600
        } else {
601 1
            foreach ($this->queryParameters as $key => $value) {
602
                // remove all group.* settings
603 1
                if (GeneralUtility::isFirstPartOfStr($key, 'group')) {
604 1
                    unset($this->queryParameters[$key]);
605
                }
606
            }
607
        }
608 2
    }
609
610
    /**
611
     * Sets the number of groups to return per group field or group query
612
     *
613
     * Internally uses the rows parameter.
614
     *
615
     * @param int $numberOfGroups Number of groups per group.field or group.query
616
     */
617 1
    public function setNumberOfGroups($numberOfGroups)
618
    {
619 1
        $this->setResultsPerPage($numberOfGroups);
620 1
    }
621
622
    /**
623
     * Gets the number of groups to return per group field or group query
624
     *
625
     * Internally uses the rows parameter.
626
     *
627
     * @return int Number of groups per group.field or group.query
628
     */
629 1
    public function getNumberOfGroups()
630
    {
631 1
        return $this->getResultsPerPage();
632
    }
633
634
    /**
635
     * Returns the number of results that should be shown per page
636
     *
637
     * @return int number of results to show per page
638
     */
639 27
    public function getResultsPerPage()
640
    {
641 27
        return $this->resultsPerPage;
642
    }
643
644
    /**
645
     * Sets the number of results that should be shown per page
646
     *
647
     * @param int $resultsPerPage Number of results to show per page
648
     * @return void
649
     */
650 34
    public function setResultsPerPage($resultsPerPage)
651
    {
652 34
        $this->resultsPerPage = max(intval($resultsPerPage), 0);
653 34
    }
654
655
    /**
656
     * Adds a field that should be used for grouping.
657
     *
658
     * @param string $fieldName Name of a field for grouping
659
     */
660 1
    public function addGroupField($fieldName)
661
    {
662 1
        $this->appendToArrayQueryParameter('group.field', $fieldName);
663 1
    }
664
665
    /**
666 1
     * Gets the fields set for grouping.
667 1
     *
668
     * @return array An array of fields set for grouping.
669
     */
670
    public function getGroupFields()
671
    {
672
        return (array)$this->getQueryParameter('group.field', []);
673
    }
674 1
675
    /**
676 1
     * Adds sorting configuration for grouping.
677
     *
678
     * @param string $sorting value of sorting configuration
679
     */
680
    public function addGroupSorting($sorting)
681
    {
682
        $this->appendToArrayQueryParameter('group.sort', $sorting);
683
    }
684 1
685
    /**
686 1
     * Gets the sorting set for grouping.
687 1
     *
688
     * @return array An array of sorting configurations for grouping.
689 1
     */
690 1
    public function getGroupSortings()
691
    {
692
        return (array)$this->getQueryParameter('group.sort', []);
693
    }
694
695
    // faceting
696
697 1
    /**
698
     * Adds a query that should be used for grouping.
699 1
     *
700
     * @param string $query Lucene query for grouping
701
     */
702
    public function addGroupQuery($query)
703
    {
704
        $this->appendToArrayQueryParameter('group.query', $query);
705
    }
706
707
    /**
708
     * Gets the queries set for grouping.
709 1
     *
710
     * @return array An array of queries set for grouping.
711 1
     */
712 1
    public function getGroupQueries()
713
    {
714
        return (array)$this->getQueryParameter('group.query', []);
715 1
    }
716 1
717
    /**
718
     * Sets the maximum number of results to be returned per group.
719
     *
720
     * @param int $numberOfResults Maximum number of results per group to return
721
     */
722
    public function setNumberOfResultsPerGroup($numberOfResults)
723 1
    {
724
        $numberOfResults = max(intval($numberOfResults), 0);
725 1
726
        $this->queryParameters['group.limit'] = $numberOfResults;
727
    }
728
729
    // filter
730
731
    /**
732
     * Gets the maximum number of results to be returned per group.
733 1
     *
734
     * @return int Maximum number of results per group to return
735 1
     */
736
    public function getNumberOfResultsPerGroup()
737 1
    {
738 1
        // default if nothing else set is 1, @see http://wiki.apache.org/solr/FieldCollapsing
739
        $numberOfResultsPerGroup = 1;
740
741
        if (!empty($this->queryParameters['group.limit'])) {
742
            $numberOfResultsPerGroup = $this->queryParameters['group.limit'];
743
        }
744
745
        return $numberOfResultsPerGroup;
746
    }
747 1
748
    /**
749
     * Activates and deactivates faceting for the current query.
750 1
     *
751
     * @param bool $faceting TRUE to enable faceting, FALSE to disable faceting
752 1
     * @return void
753 1
     */
754
    public function setFaceting($faceting = true)
755
    {
756 1
        if ($faceting) {
757
            $this->queryParameters['facet'] = 'true';
758
            $this->queryParameters['facet.mincount'] = $this->solrConfiguration->getSearchFacetingMinimumCount();
759
            $this->queryParameters['facet.limit'] = $this->solrConfiguration->getSearchFacetingFacetLimit();
760
761
            $this->applyConfiguredFacetSorting();
762
        } else {
763
            $this->removeFacetingParametersFromQuery();
764
        }
765 33
    }
766
767 33
    /**
768 33
     * Removes all facet.* or f.*.facet.* parameters from the query.
769 33
     *
770 33
     * @return void
771
     */
772 33
    protected function removeFacetingParametersFromQuery()
773
    {
774 1
        foreach ($this->queryParameters as $key => $value) {
775
            // remove all facet.* settings
776 33
            if (GeneralUtility::isFirstPartOfStr($key, 'facet')) {
777
                unset($this->queryParameters[$key]);
778
            }
779
780
            // remove all f.*.facet.* settings (overrides for individual fields)
781
            if (GeneralUtility::isFirstPartOfStr($key, 'f.') && strpos($key, '.facet.') !== false) {
782
                unset($this->queryParameters[$key]);
783 1
            }
784
        }
785 1
    }
786
787 1
    /**
788 1
     * Reads the facet sorting configuration and applies it to the queryParameters.
789
     *
790
     * @return void
791
     */
792 1
    protected function applyConfiguredFacetSorting()
793 1
    {
794
        $sorting = $this->solrConfiguration->getSearchFacetingSortBy();
795
        if (!GeneralUtility::inList('count,index,alpha,lex,1,0,true,false', $sorting)) {
796 1
            // when the sorting is not in the list of valid values we do not apply it.
797
            return;
798
        }
799
800
        // alpha and lex alias for index
801
        if ($sorting == 'alpha' || $sorting == 'lex') {
802
            $sorting = 'index';
803 33
        }
804
805 33
        $this->queryParameters['facet.sort'] = $sorting;
806 33
    }
807
808 10
    /**
809
     * Sets facet fields for a query.
810
     *
811
     * @param array $facetFields Array of field names
812 23
     */
813 1
    public function setFacetFields(array $facetFields)
814
    {
815
        $this->queryParameters['facet.field'] = [];
816 23
817 23
        foreach ($facetFields as $facetField) {
818
            $this->addFacetField($facetField);
819
        }
820
    }
821
822
    /**
823
     * Adds a single facet field.
824 1
     *
825
     * @param string $facetField field name
826 1
     */
827
    public function addFacetField($facetField)
828 1
    {
829 1
        $this->appendToArrayQueryParameter('facet.field', $facetField);
830
    }
831 1
832
    /**
833
     * Removes a filter on a field
834
     *
835
     * @param string $filterFieldName The field name the filter should be removed for
836
     * @return void
837
     */
838 2
    public function removeFilter($filterFieldName)
839
    {
840 2
        foreach ($this->filters as $key => $filterString) {
841 2
            if (GeneralUtility::isFirstPartOfStr($filterString,
842
                $filterFieldName . ':')
843
            ) {
844
                unset($this->filters[$key]);
845
            }
846
        }
847
    }
848
849 1
    /**
850
     * Removes a filter based on key of filter array
851 1
     *
852 1
     * @param string $key array key
853 1
     */
854
    public function removeFilterByKey($key)
855 1
    {
856
        unset($this->filters[$key]);
857
    }
858 1
859
    /**
860
     * Removes a filter by the filter value. The value has the following format:
861
     *
862
     * "fieldname:value"
863
     *
864
     * @param string $filterString The filter to remove, in the form of field:value
865 1
     */
866
    public function removeFilterByValue($filterString)
867 1
    {
868 1
        $key = array_search($filterString, $this->filters);
869
        if ($key === false) {
870
            // value not found, nothing to do
871
            return;
872
        }
873
        unset($this->filters[$key]);
874
    }
875
876
    /**
877 1
     * Gets all currently applied filters.
878
     *
879 1
     * @return array Array of filters
880 1
     */
881
    public function getFilters()
882
    {
883
        return $this->filters;
884 1
    }
885 1
886
    // sorting
887
888
    /**
889
     * Sets access restrictions for a frontend user.
890
     *
891
     * @param array $groups Array of groups a user has been assigned to
892 12
     */
893
    public function setUserAccessGroups(array $groups)
894 12
    {
895
        $groups = array_map('intval', $groups);
896
        $groups[] = 0; // always grant access to public documents
897
        $groups = array_unique($groups);
898
        sort($groups, SORT_NUMERIC);
899
900
        $accessFilter = '{!typo3access}' . implode(',', $groups);
901
902
        foreach ($this->filters as $key => $filter) {
903
            if (GeneralUtility::isFirstPartOfStr($filter, '{!typo3access}')) {
904 31
                unset($this->filters[$key]);
905
            }
906 31
        }
907 31
908 31
        $this->addFilter($accessFilter);
909 31
    }
910
911 31
    /**
912
     * Adds a filter parameter.
913 31
     *
914 27
     * @param string $filterString The filter to add, in the form of field:value
915 27
     * @return void
916
     */
917
    public function addFilter($filterString)
918
    {
919 31
        // TODO refactor to split filter field and filter value, @see Drupal
920 31
        if ($this->solrConfiguration->getLoggingQueryFilters()) {
921
            $this->logger->log(
922
                SolrLogManager::INFO,
923
                'Adding filter',
924
                [
925
                    $filterString
926
                ]
927
            );
928 39
        }
929
930
        $this->filters[] = $filterString;
931 39
    }
932
933
934
    // query parameters
935
936
    /**
937
     * Limits the query to certain sites
938
     *
939
     * @param string $allowedSites Comma-separated list of domains
940
     */
941 39
    public function setSiteHashFilter($allowedSites)
942 39
    {
943
        if (trim($allowedSites) === '*') {
944
            return;
945
        }
946
947
        $allowedSites = GeneralUtility::trimExplode(',', $allowedSites);
948
        $filters = [];
949
950
        foreach ($allowedSites as $site) {
951
            $siteHash = $this->siteHashService->getSiteHashForDomain($site);
952 28
            $filters[] = 'siteHash:"' . $siteHash . '"';
953
        }
954 28
955 1
        $this->addFilter(implode(' OR ', $filters));
956
    }
957
958 27
    /**
959 27
     * Limits the query to certain page tree branches
960
     *
961 27
     * @param string $pageIds Comma-separated list of page IDs
962 27
     */
963 27
    public function setRootlineFilter($pageIds)
964
    {
965
        $pageIds = GeneralUtility::trimExplode(',', $pageIds);
966 27
        $filters = [];
967 27
968
            /** @var $processor PageUidToHierarchy */
969
        $processor = GeneralUtility::makeInstance(PageUidToHierarchy::class);
970
        $hierarchies = $processor->process($pageIds);
971
972
        foreach ($hierarchies as $hierarchy) {
973
            $lastLevel = array_pop($hierarchy);
974
            $filters[] = 'rootline:"' . $lastLevel . '"';
975
        }
976
977
        $this->addFilter(implode(' OR ', $filters));
978
    }
979
980
    /**
981
     * Gets the list of fields a query will return.
982
     *
983
     * @return array Array of field names the query will return
984
     */
985
    public function getFieldList()
986
    {
987
        return $this->fieldList;
988
    }
989
990
    /**
991
     * Sets the fields to return by a query.
992
     *
993
     * @param array|string $fieldList an array or comma-separated list of field names
994
     * @throws \UnexpectedValueException on parameters other than comma-separated lists and arrays
995
     */
996 6
    public function setFieldList($fieldList = ['*', 'score'])
997
    {
998 6
        if (is_string($fieldList)) {
999
            $fieldList = GeneralUtility::trimExplode(',', $fieldList);
1000
        }
1001
1002
        if (!is_array($fieldList) || empty($fieldList)) {
1003
            throw new \UnexpectedValueException(
1004
                'Field list must be a comma-separated list or array and must not be empty.',
1005
                1310740308
1006
            );
1007 3
        }
1008
1009 3
        $this->fieldList = $fieldList;
1010 2
    }
1011
1012
    /**
1013 3
     * Gets the query type, Solr's qt parameter.
1014 1
     *
1015 1
     * @return string Query type, qt parameter.
1016 1
     */
1017
    public function getQueryType()
1018
    {
1019
        return $this->queryParameters['qt'];
1020 3
    }
1021 3
1022
    /**
1023
     * Sets the query type, Solr's qt parameter.
1024
     *
1025
     * @param string|bool $queryType String query type or boolean FALSE to disable / reset the qt parameter.
1026
     * @see http://wiki.apache.org/solr/CoreQueryParameters#qt
1027
     */
1028 1
    public function setQueryType($queryType)
1029
    {
1030 1
        $this->setQueryParameterWhenStringOrUnsetWhenEmpty('qt', $queryType);
1031
    }
1032
1033
    /**
1034
     * Sets the query operator to AND or OR. Unsets the query operator (actually
1035
     * sets it back to default) for FALSE.
1036
     *
1037
     * @param string|bool $operator AND or OR, FALSE to unset
1038
     */
1039 2
    public function setOperator($operator)
1040
    {
1041 2
        if (in_array($operator, [self::OPERATOR_AND, self::OPERATOR_OR])) {
1042 2
            $this->queryParameters['q.op'] = $operator;
1043
        }
1044 1
1045
        if ($operator === false) {
1046 2
            unset($this->queryParameters['q.op']);
1047
        }
1048
    }
1049
1050
    /**
1051
     * Gets the alternative query, Solr's q.alt parameter.
1052
     *
1053
     * @return string Alternative query, q.alt parameter.
1054 1
     */
1055
    public function getAlternativeQuery()
1056 1
    {
1057 1
        return $this->queryParameters['q.alt'];
1058
    }
1059
1060 1
    /**
1061 1
     * Sets an alternative query, Solr's q.alt parameter.
1062
     *
1063 1
     * This query supports the complete Lucene Query Language.
1064
     *
1065
     * @param mixed $alternativeQuery String alternative query or boolean FALSE to disable / reset the q.alt parameter.
1066
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#q.alt
1067
     */
1068
    public function setAlternativeQuery($alternativeQuery)
1069
    {
1070 1
        $this->setQueryParameterWhenStringOrUnsetWhenEmpty('q.alt', $alternativeQuery);
1071
    }
1072 1
1073
    // keywords
1074
1075
    /**
1076
     * Set the query to omit the response header
1077
     *
1078
     * @param bool $omitHeader TRUE (default) to omit response headers, FALSE to re-enable
1079
     */
1080
    public function setOmitHeader($omitHeader = true)
1081
    {
1082
        $omitHeader = ($omitHeader === true) ? 'true' : $omitHeader;
1083 28
        $this->setQueryParameterWhenStringOrUnsetWhenEmpty('omitHeader', $omitHeader);
1084
    }
1085 28
1086 28
    /**
1087
     * Get the query keywords, keywords are escaped.
1088 1
     *
1089
     * @return string query keywords
1090 28
     */
1091
    public function getKeywords()
1092
    {
1093
        return $this->keywords;
1094
    }
1095
1096
    /**
1097
     * Sets the query keywords, escapes them as needed for Solr/Lucene.
1098
     *
1099 1
     * @param string $keywords user search terms/keywords
1100
     */
1101 1
    public function setKeywords($keywords)
1102 1
    {
1103
        $this->keywords = $this->escape($keywords);
1104 1
        $this->keywordsRaw = $keywords;
1105
    }
1106 1
1107
    /**
1108
     * Gets the cleaned keywords so that it can be used in templates f.e.
1109
     *
1110
     * @return string The cleaned keywords.
1111
     */
1112
    public function getKeywordsCleaned()
1113 22
    {
1114
        return $this->cleanKeywords($this->keywordsRaw);
1115 22
    }
1116
1117
    /**
1118
     * Helper method to escape/encode keywords for use in HTML
1119
     *
1120
     * @param string $keywords Keywords to prepare for use in HTML
1121
     * @return string Encoded keywords
1122
     */
1123 125
    public static function cleanKeywords($keywords)
1124
    {
1125 125
        $keywords = trim($keywords);
1126 125
        $keywords = GeneralUtility::removeXSS($keywords);
1127 125
        $keywords = htmlentities($keywords, ENT_QUOTES,
1128
            $GLOBALS['TSFE']->metaCharset);
1129
1130
        // escape triple hashes as they are used in the template engine
1131
        // TODO remove after switching to fluid templates
1132
        $keywords = static::escapeMarkers($keywords);
1133
1134 22
        return $keywords;
1135
    }
1136 22
1137
    /**
1138
     * Escapes marker hashes and the pipe symbol so that they will not be
1139
     * executed in templates.
1140
     *
1141
     * @param string $content Content potentially containing markers
1142
     * @return string Content with markers escaped
1143
     */
1144
    protected static function escapeMarkers($content)
1145 22
    {
1146
        // escape marker hashes
1147 22
        $content = str_replace('###', '&#35;&#35;&#35;', $content);
1148 22
        // escape pipe character used for parameter separation
1149 22
        $content = str_replace('|', '&#124;', $content);
1150 22
1151
        return $content;
1152
    }
1153
1154 22
    // relevance, matching
1155
1156 22
    /**
1157
     * Gets the raw, unescaped, unencoded keywords.
1158
     *
1159
     * USE WITH CAUTION!
1160
     *
1161
     * @return string raw keywords
1162
     */
1163
    public function getKeywordsRaw()
1164
    {
1165
        return $this->keywordsRaw;
1166 22
    }
1167
1168
    /**
1169 22
     * Sets the minimum match (mm) parameter
1170
     *
1171 22
     * @param mixed $minimumMatch Minimum match parameter as string or boolean FALSE to disable / reset the mm parameter
1172
     * @see http://wiki.apache.org/solr/DisMaxRequestHandler#mm_.28Minimum_.27Should.27_Match.29
1173 22
     */
1174
    public function setMinimumMatch($minimumMatch)
1175
    {
1176
        $this->setQueryParameterWhenStringOrUnsetWhenEmpty('mm', $minimumMatch);
1177
    }
1178
1179
    /**
1180
     * Sets the boost function (bf) parameter
1181
     *
1182
     * @param mixed $boostFunction boost function parameter as string or boolean FALSE to disable / reset the bf parameter
1183
     * @see http://wiki.apache.org/solr/DisMaxRequestHandler#bf_.28Boost_Functions.29
1184
     */
1185
    public function setBoostFunction($boostFunction)
1186
    {
1187
        $this->setQueryParameterWhenStringOrUnsetWhenEmpty('bf', $boostFunction);
1188
    }
1189
1190
    // query fields
1191
    // TODO move up to field list methods
1192
1193
    /**
1194
     * Sets the boost query (bq) parameter
1195
     *
1196 1
     * @param mixed $boostQuery boost query parameter as string or array to set a boost query or boolean FALSE to disable / reset the bq parameter
1197
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#bq_.28Boost_Query.29
1198 1
     */
1199 1
    public function setBoostQuery($boostQuery)
1200
    {
1201 1
        if (is_array($boostQuery)) {
1202
            $this->queryParameters['bq'] = $boostQuery;
1203 1
            return;
1204
        }
1205
        $this->setQueryParameterWhenStringOrUnsetWhenEmpty('bq', $boostQuery);
1206
    }
1207
1208
    /**
1209
     * Gets a specific query parameter by its name.
1210
     *
1211 1
     * @param string $parameterName The parameter to return
1212
     * @param mixed $defaultIfEmpty
1213 1
     * @return mixed The parameter's value or $defaultIfEmpty if not set
1214 1
     */
1215
    public function getQueryParameter($parameterName, $defaultIfEmpty = null)
1216 1
    {
1217
        $parameters = $this->getQueryParameters();
1218 1
        return isset($parameters[$parameterName]) ? $parameters[$parameterName] : $defaultIfEmpty;
1219
    }
1220
1221
    /**
1222
     * Builds an array of query parameters to use for the search query.
1223
     *
1224
     * @return array An array ready to use with query parameters
1225
     */
1226
    public function getQueryParameters()
1227
    {
1228
        $queryParameters = array_merge(
1229 1
            [
1230
                'fl' => implode(',', $this->fieldList),
1231 1
                'fq' => array_values($this->filters)
1232 1
            ],
1233
            $this->queryParameters
1234 1
        );
1235
1236 1
        $queryFieldString = $this->getQueryFieldsAsString();
1237
        if (!empty($queryFieldString)) {
1238
            $queryParameters['qf'] = $queryFieldString;
1239
        }
1240
1241
        return $queryParameters;
1242
    }
1243
1244
    // general query parameters
1245 10
1246
    /**
1247 10
     * Compiles the query fields into a string to be used in Solr's qf parameter.
1248 10
     *
1249
     * @return string A string of query fields with their associated boosts
1250
     */
1251
    public function getQueryFieldsAsString()
1252
    {
1253
        $queryFieldString = '';
1254
1255
        foreach ($this->queryFields as $fieldName => $fieldBoost) {
1256 67
            $queryFieldString .= $fieldName;
1257
1258 67
            if ($fieldBoost != 1.0) {
1259
                $queryFieldString .= '^' . number_format($fieldBoost, 1, '.', '');
1260 67
            }
1261 67
1262
            $queryFieldString .= ' ';
1263 67
        }
1264
1265
        return trim($queryFieldString);
1266 67
    }
1267 67
1268 30
    /**
1269
     * Enables or disables highlighting of search terms in result teasers.
1270
     *
1271 67
     * @param bool $highlighting Enables highlighting when set to TRUE, deactivates highlighting when set to FALSE, defaults to TRUE.
1272
     * @param int $fragmentSize Size, in characters, of fragments to consider for highlighting.
1273
     * @see http://wiki.apache.org/solr/HighlightingParameters
1274
     * @return void
1275
     */
1276
    public function setHighlighting($highlighting = true, $fragmentSize = 200)
1277
    {
1278
        if ($highlighting) {
1279
            $this->queryParameters['hl'] = 'true';
1280
            $this->queryParameters['hl.fragsize'] = (int)$fragmentSize;
1281 69
1282
            $highlightingFields = $this->solrConfiguration->getSearchResultsHighlightingFields();
1283 69
            if ($highlightingFields != '') {
1284
                $this->queryParameters['hl.fl'] = $highlightingFields;
1285 69
            }
1286 31
1287
            // the fast vector highlighter can only be used, when the fragmentSize is
1288 31
            // higher then 17 otherwise solr throws an exception
1289 31
            $useFastVectorHighlighter = ($fragmentSize >= 18);
1290
            $wrap = explode('|', $this->solrConfiguration->getSearchResultsHighlightingWrap());
1291
1292 31
            if ($useFastVectorHighlighter) {
1293
                $this->queryParameters['hl.useFastVectorHighlighter'] = 'true';
1294
                $this->queryParameters['hl.tag.pre'] = $wrap[0];
1295 69
                $this->queryParameters['hl.tag.post'] = $wrap[1];
1296
            }
1297
1298
            if (isset($wrap[0]) && isset($wrap[1])) {
1299
                $this->queryParameters['hl.simple.pre'] = $wrap[0];
1300
                $this->queryParameters['hl.simple.post'] = $wrap[1];
1301
            }
1302
        } else {
1303
            // remove all hl.* settings
1304
            foreach ($this->queryParameters as $key => $value) {
1305
                if (GeneralUtility::isFirstPartOfStr($key, 'hl')) {
1306 33
                    unset($this->queryParameters[$key]);
1307
                }
1308 33
            }
1309 33
        }
1310 33
    }
1311
1312 33
    // misc
1313 33
1314 27
    /**
1315
     * Enables or disables spellchecking for the query.
1316
     *
1317
     * @param bool $spellchecking Enables spellchecking when set to TRUE, deactivates spellchecking when set to FALSE, defaults to TRUE.
1318
     */
1319 33
    public function setSpellchecking($spellchecking = true)
1320 33
    {
1321
        if ($spellchecking) {
1322 33
            $this->queryParameters['spellcheck'] = 'true';
1323 31
            $this->queryParameters['spellcheck.collate'] = 'true';
1324 31
            $maxCollationTries = $this->solrConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry();
1325 31
            $this->addQueryParameter('spellcheck.maxCollationTries', $maxCollationTries);
1326
        } else {
1327
            unset($this->queryParameters['spellcheck']);
1328 33
            unset($this->queryParameters['spellcheck.collate']);
1329 28
            unset($this->queryParameters['spellcheck.maxCollationTries']);
1330 33
        }
1331
    }
1332
1333
    /**
1334 1
     * This method can be used to set a query parameter when the value is a string and not empty or unset it
1335 1
     * in any other case. Extracted to avoid duplicate code.
1336 1
     *
1337
     * @param string $parameterName
1338
     * @param mixed $value
1339
     */
1340 33
    private function setQueryParameterWhenStringOrUnsetWhenEmpty($parameterName, $value)
1341
    {
1342
        if (is_string($value) && !empty($value)) {
1343
            $this->addQueryParameter($parameterName, $value);
1344
        } else {
1345
            unset($this->queryParameters[$parameterName]);
1346
        }
1347
    }
1348
1349 24
    /**
1350
     * Adds any generic query parameter.
1351 24
     *
1352 24
     * @param string $parameterName Query parameter name
1353 24
     * @param string $parameterValue Parameter value
1354 24
     */
1355 24
    public function addQueryParameter($parameterName, $parameterValue)
1356
    {
1357 1
        $this->queryParameters[$parameterName] = $parameterValue;
1358 1
    }
1359 1
1360
    /**
1361 24
     * Appends an item to a queryParameter that is an array or initializes it as empty array when it is not set.
1362
     *
1363
     * @param string $key
1364
     * @param mixed $value
1365
     */
1366
    private function appendToArrayQueryParameter($key, $value)
1367
    {
1368
        if (!isset($this->queryParameters[$key])) {
1369 32
            $this->queryParameters[$key] = [];
1370
        }
1371 32
1372 32
        $this->queryParameters[$key][] = $value;
1373
    }
1374
1375
    /**
1376
     * Sets the sort parameter.
1377
     *
1378
     * $sorting must include a field name (or the pseudo-field score),
1379
     * followed by a space,
1380
     * followed by a sort direction (asc or desc).
1381
     *
1382
     * Multiple fallback sortings can be separated by comma,
1383
     * ie: <field name> <direction>[,<field name> <direction>]...
1384
     *
1385
     * @param string|bool $sorting Either a comma-separated list of sort fields and directions or FALSE to reset sorting to the default behavior (sort by score / relevance)
1386
     * @see http://wiki.apache.org/solr/CommonQueryParameters#sort
1387 2
     */
1388
    public function setSorting($sorting)
1389 2
    {
1390 2
        if ($sorting) {
1391
            if (!is_string($sorting)) {
1392
                throw new \InvalidArgumentException('Sorting needs to be a string!');
1393 2
            }
1394 2
            $sortParameter = $this->removeRelevanceSortField($sorting);
1395
            $this->queryParameters['sort'] = $sortParameter;
1396 1
        } else {
1397
            unset($this->queryParameters['sort']);
1398 2
        }
1399
    }
1400
1401
    /**
1402
     * Removes the relevance sort field if present in the sorting field definition.
1403
     *
1404
     * @param string $sorting
1405
     * @return string
1406 2
     */
1407
    protected function removeRelevanceSortField($sorting)
1408 2
    {
1409 2
        $sortParameter = $sorting;
1410 2
        list($sortField) = explode(' ', $sorting);
1411 1
        if ($sortField == 'relevance') {
1412 1
            $sortParameter = '';
1413
            return $sortParameter;
1414
        }
1415 2
1416
        return $sortParameter;
1417
    }
1418
1419
    /**
1420
     * Enables or disables the debug parameter for the query.
1421
     *
1422
     * @param bool $debugMode Enables debugging when set to TRUE, deactivates debugging when set to FALSE, defaults to TRUE.
1423 22
     */
1424
    public function setDebugMode($debugMode = true)
1425 22
    {
1426 22
        if ($debugMode) {
1427 22
            $this->queryParameters['debugQuery'] = 'true';
1428
            $this->queryParameters['echoParams'] = 'all';
1429
        } else {
1430
            unset($this->queryParameters['debugQuery']);
1431
            unset($this->queryParameters['echoParams']);
1432 22
        }
1433
    }
1434
1435
    /**
1436
     * Returns the link target page id.
1437
     *
1438
     * @return int
1439 2
     */
1440
    public function getLinkTargetPageId()
1441 2
    {
1442
        return $this->linkTargetPageId;
1443
    }
1444
1445
    /**
1446
     * Activates the collapsing on the configured field, if collapsing was enabled.
1447
     *
1448
     * @return bool
1449 124
     */
1450
    protected function initializeCollapsingFromConfiguration()
1451
    {
1452 124
        // check collapsing
1453 3
        if ($this->solrConfiguration->getSearchVariants()) {
1454 3
            $collapseField = $this->solrConfiguration->getSearchVariantsField();
1455 3
            $this->setVariantField($collapseField);
1456
            $this->setCollapsing(true);
1457 3
1458
            return true;
1459
        }
1460 121
1461
        return false;
1462
    }
1463
}
1464