Completed
Push — master ( 2529b9...694264 )
by Timo
13:23
created

Query::addSortField()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

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