Completed
Push — master ( 2bc4b1...eec5d7 )
by Timo
40s
created

Query::setCollapsing()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3.1406

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 9
cts 12
cp 0.75
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 10
nc 3
nop 1
crap 3.1406
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\System\Configuration\TypoScriptConfiguration;
28
use TYPO3\CMS\Core\Utility\GeneralUtility;
29
30
/**
31
 * A Solr search query
32
 *
33
 * @author Ingo Renner <[email protected]>
34
 */
35
class Query
36
{
37
38
    // FIXME extract link building from the query, it's not the query's domain
39
40
    const SORT_ASC = 'ASC';
41
    const SORT_DESC = 'DESC';
42
43
    const OPERATOR_AND = 'AND';
44
    const OPERATOR_OR = 'OR';
45
46
    /**
47
     * Used to identify the queries.
48
     *
49
     * @var int
50
     */
51
    protected static $idCount = 0;
52
53
    /**
54
     * @var int
55
     */
56
    protected $id;
57
58
    /**
59
     * @var TypoScriptConfiguration
60
     */
61
    protected $solrConfiguration;
62
63
    /**
64
     * @var string
65
     */
66
    protected $keywords;
67
68
    /**
69
     * @var string
70
     */
71
    protected $keywordsRaw;
72
73
    /**
74
     * @var array
75
     */
76
    protected $filters = [];
77
78
    /**
79
     * @var string
80
     */
81
    protected $sorting;
82
83
    // TODO check usage of these two variants, especially the check for $rawQueryString in getQueryString()
84
    /**
85
     * @var
86
     */
87
    protected $queryString;
88
89
    /**
90
     * @var array
91
     */
92
    protected $queryParameters = [];
93
94
    /**
95
     * @var int
96
     */
97
    protected $resultsPerPage;
98
99
    /**
100
     * @var int
101
     */
102
    protected $page;
103
104
    /**
105
     * @var int
106
     */
107
    protected $linkTargetPageId;
108
109
    /**
110
     * Holds the query fields with their associated boosts. The key represents
111
     * the field name, value represents the field's boost. These are the fields
112
     * that will actually be searched.
113
     *
114
     * Used in Solr's qf parameter
115
     *
116
     * @var array
117
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#qf_.28Query_Fields.29
118
     */
119
    protected $queryFields = [];
120
121
    /**
122
     * List of fields that will be returned in the result documents.
123
     *
124
     * used in Solr's fl parameter
125
     *
126
     * @var array
127
     * @see http://wiki.apache.org/solr/CommonQueryParameters#fl
128
     */
129
    protected $fieldList = [];
130
131
    /**
132
     * @var array
133
     */
134
    protected $filterFields;
135
136
    /**
137
     * @var array
138
     */
139
    protected $sortingFields;
140
141
    /**
142
     * @var bool
143
     */
144
    private $rawQueryString = false;
145
146
    /**
147
     * The field by which the result will be collapsed
148
     * @var string
149
     */
150
    protected $variantField = 'variantId';
151
152
    /**
153
     * @param string $keywords
154
     * @param TypoScriptConfiguration $solrConfiguration
155
     */
156 97
    public function __construct($keywords, $solrConfiguration = null)
157
    {
158 97
        $keywords = (string) $keywords;
159 97
        if ($solrConfiguration == null) {
160 53
            $this->solrConfiguration = Util::getSolrConfiguration();
161 53
        } else {
162 44
            $this->solrConfiguration = $solrConfiguration;
163
        }
164
165 97
        $this->setKeywords($keywords);
166 97
        $this->sorting = '';
167
168
        // What fields to search
169 97
        $queryFields = $this->solrConfiguration->getSearchQueryQueryFields();
170 97
        if ($queryFields != '') {
171 25
            $this->setQueryFieldsFromString($queryFields);
172 25
        }
173
174
        // What fields to return from Solr
175 97
        $this->fieldList = $this->solrConfiguration->getSearchQueryReturnFieldsAsArray(array('*', 'score'));
176 97
        $this->linkTargetPageId = $this->solrConfiguration->getSearchTargetPage();
177
178 97
        $this->initializeQuery();
179
180 97
        $this->id = ++self::$idCount;
181 97
    }
182
183
    /**
184
     * @return void
185
     */
186 96
    protected function initializeQuery()
187
    {
188 96
        $this->initializeCollapsingFromConfiguration();
189 96
    }
190
191
    /**
192
     * Writes a message to the devLog.
193
     *
194
     * @param string $msg
195
     * @param int $severity
196
     * @param bool $dataVar
197
     */
198
    protected function writeDevLog($msg, $severity = 0, $dataVar = false)
199
    {
200
        GeneralUtility::devLog($msg, 'solr', $severity, $dataVar);
201
    }
202
203
    /**
204
     * Takes a string of comma separated query fields and _overwrites_ the
205
     * currently set query fields. Boost can also be specified in through the
206
     * given string.
207
     *
208
     * Example: "title^5, subtitle^2, content, author^0.5"
209
     * This sets the query fields to title with  a boost of 5.0, subtitle with
210
     * a boost of 2.0, content with a default boost of 1.0 and the author field
211
     * with a boost of 0.5
212
     *
213
     * @param string $queryFields A string defining which fields to query and their associated boosts
214
     * @return void
215
     */
216 26
    public function setQueryFieldsFromString($queryFields)
217
    {
218 26
        $fields = GeneralUtility::trimExplode(',', $queryFields, true);
219
220 26
        foreach ($fields as $field) {
221 26
            $fieldNameAndBoost = explode('^', $field);
222
223 26
            $boost = 1.0;
224 26
            if (isset($fieldNameAndBoost[1])) {
225 26
                $boost = floatval($fieldNameAndBoost[1]);
226 26
            }
227
228 26
            $this->setQueryField($fieldNameAndBoost[0], $boost);
229 26
        }
230 26
    }
231
232
    /**
233
     * Sets a query field and its boost. If the field does not exist yet, it
234
     * gets added. Boost is optional, if left out a default boost of 1.0 is
235
     * applied.
236
     *
237
     * @param string $fieldName The field's name
238
     * @param float $boost Optional field boost, defaults to 1.0
239
     * @return void
240
     */
241 26
    public function setQueryField($fieldName, $boost = 1.0)
242
    {
243 26
        $this->queryFields[$fieldName] = (float)$boost;
244 26
    }
245
246
    /**
247
     * magic implementation for clone(), makes sure that the id counter is
248
     * incremented
249
     *
250
     * @return void
251
     */
252 22
    public function __clone()
253 22
    {
254 5
        $this->id = ++self::$idCount;
255 5
    }
256
257
    /**
258
     * returns a string representation of the query
259
     *
260
     * @return string the string representation of the query
261
     */
262 7
    public function __toString()
263
    {
264 7
        return $this->getQueryString();
265
    }
266
267
    /**
268
     * Builds the query string which is then used for Solr's q parameters
269
     *
270
     * @return string Solr query string
271
     */
272 32
    public function getQueryString()
273
    {
274 32
        if (!$this->rawQueryString) {
275 31
            $this->buildQueryString();
276 31
        }
277
278 32
        return $this->queryString;
279
    }
280
281
    /**
282
     * Sets the query string without any escaping.
283
     *
284
     * Be cautious with this function!
285
     * TODO remove this method as it basically just sets the q parameter / keywords
286
     *
287
     * @param string $queryString The raw query string.
288
     */
289 1
    public function setQueryString($queryString)
290
    {
291 1
        $this->queryString = $queryString;
292 1
    }
293
294
    /**
295
     * Creates the string that is later used as the q parameter in the solr query
296
     *
297
     * @return void
298
     */
299 31
    protected function buildQueryString()
300
    {
301
        // very simple for now
302 31
        $this->queryString = $this->keywords;
303 31
    }
304
305
    /**
306
     * Sets whether a raw query sting should be used, that is, whether the query
307
     * string should be escaped or not.
308
     *
309
     * @param bool $useRawQueryString TRUE to use raw queries (like Lucene Query Language) or FALSE for regular, escaped queries
310
     */
311 1
    public function useRawQueryString($useRawQueryString)
312
    {
313 1
        $this->rawQueryString = (boolean)$useRawQueryString;
314 1
    }
315
316
    /**
317
     * Returns the query's ID.
318
     *
319
     * @return int The query's ID.
320
     */
321
    public function getId()
322
    {
323
        return $this->id;
324
    }
325
326
    /**
327
     * Quote and escape search strings
328
     *
329
     * @param string $string String to escape
330
     * @return string The escaped/quoted string
331
     */
332 97
    public function escape($string)
333
    {
334
        // when we have a numeric string only, nothing needs to be done
335 97
        if (is_numeric($string)) {
336 1
            return $string;
337
        }
338
339
        // when no whitespaces are in the query we can also just escape the special characters
340 97
        if (preg_match('/\W/', $string) != 1) {
341 82
            return $this->escapeSpecialCharacters($string);
342
        }
343
344
        // when there are no quotes inside the query string we can also just escape the whole string
345 31
        $hasQuotes = strrpos($string, '"') !== false;
346 31
        if (!$hasQuotes) {
347 24
            return $this->escapeSpecialCharacters($string);
348
        }
349
350 7
        $result = $this->tokenizeByQuotesAndEscapeDependingOnContext($string);
351
352 7
        return $result;
353
    }
354
355
    /**
356
     * This method is used to escape the content in the query string surrounded by quotes
357
     * different then when it is not in a quoted context.
358
     *
359
     * @param string $string
360
     * @return string
361
     */
362 7
    protected function tokenizeByQuotesAndEscapeDependingOnContext($string)
363
    {
364 7
        $result = '';
365 7
        $quotesCount = substr_count($string, '"');
366 7
        $isEvenAmountOfQuotes = $quotesCount % 2 === 0;
367
368
        // go over all quote segments and apply escapePhrase inside a quoted
369
        // context and escapeSpecialCharacters outside the quoted context.
370 7
        $segments = explode('"', $string);
371 7
        $segmentsIndex = 0;
372 7
        foreach ($segments as $segment) {
373 7
            $isInQuote = $segmentsIndex % 2 !== 0;
374 7
            $isLastQuote = $segmentsIndex === $quotesCount;
375
376 7
            if ($isLastQuote && !$isEvenAmountOfQuotes) {
377 1
                $result .= '\"';
378 1
            }
379
380 7
            if ($isInQuote && !$isLastQuote) {
381 6
                $result .= $this->escapePhrase($segment);
382 6
            } else {
383 7
                $result .= $this->escapeSpecialCharacters($segment);
384
            }
385
386 7
            $segmentsIndex++;
387 7
        }
388
389 7
        return $result;
390
    }
391
392
    // pagination
393
394
    /**
395
     * Escapes a value meant to be contained in a phrase with characters with
396
     * special meanings in Lucene query syntax.
397
     *
398
     * @param string $value Unescaped - "dirty" - string
399
     * @return string Escaped - "clean" - string
400
     */
401 6
    protected function escapePhrase($value)
402
    {
403 6
        $pattern = '/("|\\\)/';
404 6
        $replace = '\\\$1';
405
406 6
        return '"' . preg_replace($pattern, $replace, $value) . '"';
407
    }
408
409
    /**
410
     * Escapes characters with special meanings in Lucene query syntax.
411
     *
412
     * @param string $value Unescaped - "dirty" - string
413
     * @return string Escaped - "clean" - string
414
     */
415 97
    protected function escapeSpecialCharacters($value)
416
    {
417
        // list taken from http://lucene.apache.org/core/4_4_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package_description
418
        // which mentions: + - && || ! ( ) { } [ ] ^ " ~ * ? : \ /
419
        // of which we escape: ( ) { } [ ] ^ " ~ : \ /
420
        // and explicitly don't escape: + - && || ! * ?
421 97
        $pattern = '/(\\(|\\)|\\{|\\}|\\[|\\]|\\^|"|~|\:|\\\\|\\/)/';
422 97
        $replace = '\\\$1';
423
424 97
        return preg_replace($pattern, $replace, $value);
425
    }
426
427
    /**
428
     * Gets the currently showing page's number
429
     *
430
     * @return int page number currently showing
431
     */
432
    public function getPage()
433
    {
434
        return $this->page;
435
    }
436
437
    /**
438
     * Sets the page that should be shown
439
     *
440
     * @param int $page page number to show
441
     * @return void
442
     */
443
    public function setPage($page)
444
    {
445
        $this->page = max(intval($page), 0);
446
    }
447
448
    /**
449
     * Gets the index of the first result document we're showing
450
     *
451
     * @return int index of the currently first document showing
452
     */
453
    public function getStartIndex()
454
    {
455
        return ($this->page - 1) * $this->resultsPerPage;
456
    }
457
458
    /**
459
     * Gets the index of the last result document we're showing
460
     *
461
     * @return int index of the currently last document showing
462
     */
463
    public function getEndIndex()
464
    {
465
        return $this->page * $this->resultsPerPage;
466
    }
467
468
    // query elevation
469
470
    /**
471
     * Activates and deactivates query elevation for the current query.
472
     *
473
     * @param bool $elevation True to enable query elevation (default), FALSE to disable query elevation.
474
     * @param bool $forceElevation Optionally force elevation so that the elevated documents are always on top regardless of sorting, default to TRUE.
475
     * @param bool $markElevatedResults Mark elevated results
476
     * @return void
477
     */
478 23
    public function setQueryElevation(
479
        $elevation = true,
480
        $forceElevation = true,
481
        $markElevatedResults = true
482
    ) {
483 23
        if ($elevation) {
484 20
            $this->queryParameters['enableElevation'] = 'true';
485
486 20
            if ($forceElevation) {
487 20
                $this->queryParameters['forceElevation'] = 'true';
488 20
            } else {
489
                $this->queryParameters['forceElevation'] = 'false';
490
            }
491
492 20
            if ($markElevatedResults) {
493 20
                $this->addReturnField('isElevated:[elevated]');
494 20
            }
495 20
        } else {
496 3
            $this->queryParameters['enableElevation'] = 'false';
497 3
            unset($this->queryParameters['forceElevation']);
498 3
            $this->removeReturnField('isElevated:[elevated]');
499 3
            $this->removeReturnField('[elevated]'); // fallback
500
        }
501 23
    }
502
503
    // collapsing
504
505
    /**
506
     * Check whether collapsing is active
507
     *
508
     * @return bool
509
     */
510 3
    public function getIsCollapsing()
511
    {
512 3
        return array_key_exists('collapsing', $this->filters);
513
    }
514
515
    /**
516
     * @param string $fieldName
517
     */
518 2
    public function setVariantField($fieldName)
519
    {
520 2
        $this->variantField = $fieldName;
521 2
    }
522
523
    /**
524
     * @return string
525
     */
526 1
    public function getVariantField()
527
    {
528 1
        return $this->variantField;
529
    }
530
531
    /**
532
     * @param bool $collapsing
533
     */
534 2
    public function setCollapsing($collapsing = true)
535
    {
536 2
        if ($collapsing) {
537 2
            $this->filters['collapsing'] = '{!collapse field=' . $this->variantField . '}';
538 2
            if ($this->solrConfiguration->getSearchVariantsExpand()) {
539 1
                $this->queryParameters['expand'] = 'true';
540 1
                $this->queryParameters['expand.rows'] = $this->solrConfiguration->getSearchVariantsLimit();
541 1
            }
542 2
        } else {
543
            unset($this->filters['collapsing']);
544
            unset($this->queryParameters['expand']);
545
            unset($this->queryParameters['expand.rows']);
546
        }
547 2
    }
548
549
    // grouping
550
551
    /**
552
     * Adds a field to the list of fields to return. Also checks whether * is
553
     * set for the fields, if so it's removed from the field list.
554
     *
555
     * @param string $fieldName Name of a field to return in the result documents
556
     */
557 20
    public function addReturnField($fieldName)
558
    {
559 20
        if (strpos($fieldName, '[') === false
560 20
            && strpos($fieldName, ']') === false
561 20
            && in_array('*', $this->fieldList)
562 20
        ) {
563
            $this->fieldList = array_diff($this->fieldList, array('*'));
564
        }
565
566 20
        $this->fieldList[] = $fieldName;
567 20
    }
568
569
    /**
570
     * Removes a field from the list of fields to return (fl parameter).
571
     *
572
     * @param string $fieldName Field to remove from the list of fields to return
573
     */
574 3
    public function removeReturnField($fieldName)
575
    {
576 3
        $key = array_search($fieldName, $this->fieldList);
577
578 3
        if ($key !== false) {
579
            unset($this->fieldList[$key]);
580
        }
581 3
    }
582
583
    /**
584
     * Activates and deactivates grouping for the current query.
585
     *
586
     * @param bool $grouping TRUE to enable grouping, FALSE to disable grouping
587
     * @return void
588
     */
589 2
    public function setGrouping($grouping = true)
590
    {
591 2
        if ($grouping) {
592 1
            $this->queryParameters['group'] = 'true';
593 1
            $this->queryParameters['group.format'] = 'grouped';
594 1
            $this->queryParameters['group.ngroups'] = 'true';
595 1
        } else {
596 1
            foreach ($this->queryParameters as $key => $value) {
597
                // remove all group.* settings
598 1
                if (GeneralUtility::isFirstPartOfStr($key, 'group')) {
599 1
                    unset($this->queryParameters[$key]);
600 1
                }
601 1
            }
602
        }
603 2
    }
604
605
    /**
606
     * Sets the number of groups to return per group field or group query
607
     *
608
     * Internally uses the rows parameter.
609
     *
610
     * @param int $numberOfGroups Number of groups per group.field or group.query
611
     */
612
    public function setNumberOfGroups($numberOfGroups)
613
    {
614
        $this->setResultsPerPage($numberOfGroups);
615
    }
616
617
    /**
618
     * Gets the number of groups to return per group field or group query
619
     *
620
     * Internally uses the rows parameter.
621
     *
622
     * @return int Number of groups per group.field or group.query
623
     */
624
    public function getNumberOfGroups()
625
    {
626
        return $this->getResultsPerPage();
627
    }
628
629
    /**
630
     * Returns the number of results that should be shown per page
631
     *
632
     * @return int number of results to show per page
633
     */
634 23
    public function getResultsPerPage()
635
    {
636 23
        return $this->resultsPerPage;
637
    }
638
639
    /**
640
     * Sets the number of results that should be shown per page
641
     *
642
     * @param int $resultsPerPage Number of results to show per page
643
     * @return void
644
     */
645 30
    public function setResultsPerPage($resultsPerPage)
646
    {
647 30
        $this->resultsPerPage = max(intval($resultsPerPage), 0);
648 30
    }
649
650
    /**
651
     * Adds a field that should be used for grouping.
652
     *
653
     * @param string $fieldName Name of a field for grouping
654
     */
655
    public function addGroupField($fieldName)
656
    {
657
        if (!isset($this->queryParameters['group.field'])) {
658
            $this->queryParameters['group.field'] = array();
659
        }
660
661
        $this->queryParameters['group.field'][] = $fieldName;
662
    }
663
664
    /**
665
     * Gets the fields set for grouping.
666
     *
667
     * @return array An array of fields set for grouping.
668
     */
669
    public function getGroupFields()
670
    {
671
        $groupFields = array();
672
673
        if (isset($this->queryParameters['group.field'])) {
674
            $groupFields = $this->queryParameters['group.field'];
675
        }
676
677
        return $groupFields;
678
    }
679
680
    /**
681
     * Adds sorting configuration for grouping.
682
     *
683
     * @param string $sorting value of sorting configuration
684
     */
685
    public function addGroupSorting($sorting)
686
    {
687
        if (!isset($this->queryParameters['group.sort'])) {
688
            $this->queryParameters['group.sort'] = array();
689
        }
690
        $this->queryParameters['group.sort'][] = $sorting;
691
    }
692
693
    /**
694
     * Gets the sorting set for grouping.
695
     *
696
     * @return array An array of sorting configurations for grouping.
697
     */
698
    public function getGroupSortings()
699
    {
700
        $groupSortings = array();
701
        if (isset($this->queryParameters['group.sort'])) {
702
            $groupSortings = $this->queryParameters['group.sort'];
703
        }
704
        return $groupSortings;
705
    }
706
707
    // faceting
708
709
    /**
710
     * Adds a query that should be used for grouping.
711
     *
712
     * @param string $query Lucene query for grouping
713
     */
714
    public function addGroupQuery($query)
715
    {
716
        if (!isset($this->queryParameters['group.query'])) {
717
            $this->queryParameters['group.query'] = array();
718
        }
719
720
        $this->queryParameters['group.query'][] = $query;
721
    }
722
723
    /**
724
     * Gets the queries set for grouping.
725
     *
726
     * @return array An array of queries set for grouping.
727
     */
728
    public function getGroupQueries()
729
    {
730
        $groupQueries = array();
731
732
        if (isset($this->queryParameters['group.query'])) {
733
            $groupQueries = $this->queryParameters['group.query'];
734
        }
735
736
        return $groupQueries;
737
    }
738
739
    /**
740
     * Sets the maximum number of results to be returned per group.
741
     *
742
     * @param int $numberOfResults Maximum number of results per group to return
743
     */
744
    public function setNumberOfResultsPerGroup($numberOfResults)
745
    {
746
        $numberOfResults = max(intval($numberOfResults), 0);
747
748
        $this->queryParameters['group.limit'] = $numberOfResults;
749
    }
750
751
    // filter
752
753
    /**
754
     * Gets the maximum number of results to be returned per group.
755
     *
756
     * @return int Maximum number of results per group to return
757
     */
758
    public function getNumberOfResultsPerGroup()
759
    {
760
        // default if nothing else set is 1, @see http://wiki.apache.org/solr/FieldCollapsing
761
        $numberOfResultsPerGroup = 1;
762
763
        if (!empty($this->queryParameters['group.limit'])) {
764
            $numberOfResultsPerGroup = $this->queryParameters['group.limit'];
765
        }
766
767
        return $numberOfResultsPerGroup;
768
    }
769
770
    /**
771
     * Activates and deactivates faceting for the current query.
772
     *
773
     * @param bool $faceting TRUE to enable faceting, FALSE to disable faceting
774
     * @return void
775
     */
776 30
    public function setFaceting($faceting = true)
777
    {
778 30
        if ($faceting) {
779 30
            $this->queryParameters['facet'] = 'true';
780 30
            $this->queryParameters['facet.mincount'] = $this->solrConfiguration->getSearchFacetingMinimumCount();
781
782 30
            $sorting = $this->solrConfiguration->getSearchFacetingSortBy();
783 30
            if (GeneralUtility::inList('count,index,alpha,lex,1,0,true,false', $sorting)) {
784
                // alpha and lex alias for index
785 21
                if ($sorting == 'alpha' || $sorting == 'lex') {
786 1
                    $sorting = 'index';
787 1
                }
788
789 21
                $this->queryParameters['facet.sort'] = $sorting;
790 21
            }
791 30
        } else {
792
            foreach ($this->queryParameters as $key => $value) {
793
                // remove all facet.* settings
794
                if (GeneralUtility::isFirstPartOfStr($key, 'facet')) {
795
                    unset($this->queryParameters[$key]);
796
                }
797
798
                // remove all f.*.facet.* settings (overrides for individual fields)
799
                if (GeneralUtility::isFirstPartOfStr($key, 'f.') && strpos($key,
800
                        '.facet.') !== false
801
                ) {
802
                    unset($this->queryParameters[$key]);
803
                }
804
            }
805
        }
806 30
    }
807
808
    /**
809
     * Sets facet fields for a query.
810
     *
811
     * @param array $facetFields Array of field names
812
     */
813
    public function setFacetFields(array $facetFields)
814
    {
815
        $this->queryParameters['facet.field'] = array();
816
817
        foreach ($facetFields as $facetField) {
818
            $this->addFacetField($facetField);
819
        }
820
    }
821
822
    /**
823
     * Adds a single facet field.
824
     *
825
     * @param string $facetField field name
826
     */
827
    public function addFacetField($facetField)
828
    {
829
        $this->queryParameters['facet.field'][] = $facetField;
830
    }
831
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
    public function removeFilter($filterFieldName)
839
    {
840
        foreach ($this->filters as $key => $filterString) {
841
            if (GeneralUtility::isFirstPartOfStr($filterString,
842
                $filterFieldName . ':')
843
            ) {
844
                unset($this->filters[$key]);
845
            }
846
        }
847
    }
848
849
    /**
850
     * Removes a filter based on key of filter array
851
     *
852
     * @param string $key array key
853
     */
854
    public function removeFilterByKey($key)
855
    {
856
        unset($this->filters[$key]);
857
    }
858
859
    /**
860
     * Gets all currently applied filters.
861
     *
862
     * @return array Array of filters
863
     */
864 25
    public function getFilters()
865
    {
866 25
        return $this->filters;
867
    }
868
869
    // sorting
870
871
    /**
872
     * Sets access restrictions for a frontend user.
873
     *
874
     * @param array $groups Array of groups a user has been assigned to
875
     */
876 28
    public function setUserAccessGroups(array $groups)
877
    {
878 28
        $groups = array_map('intval', $groups);
879 28
        $groups[] = 0; // always grant access to public documents
880 28
        $groups = array_unique($groups);
881 28
        sort($groups, SORT_NUMERIC);
882
883 28
        $accessFilter = '{!typo3access}' . implode(',', $groups);
884
885 28
        foreach ($this->filters as $key => $filter) {
886 24
            if (GeneralUtility::isFirstPartOfStr($filter, '{!typo3access}')) {
887 1
                unset($this->filters[$key]);
888 1
            }
889 28
        }
890
891 28
        $this->addFilter($accessFilter);
892 28
    }
893
894
    /**
895
     * Adds a filter parameter.
896
     *
897
     * @param string $filterString The filter to add, in the form of field:value
898
     * @return void
899
     */
900 33
    public function addFilter($filterString)
901
    {
902
        // TODO refactor to split filter field and filter value, @see Drupal
903 33
        if ($this->solrConfiguration->getLoggingQueryFilters()) {
904 1
            $this->writeDevLog('adding filter', 0, array($filterString));
0 ignored issues
show
Documentation introduced by
array($filterString) is of type array<integer,string,{"0":"string"}>, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
905 1
        }
906
907 33
        $this->filters[] = $filterString;
908 33
    }
909
910
    // query parameters
911
912
    /**
913
     * Limits the query to certain sites
914
     *
915
     * @param string $allowedSites Comma-separated list of domains
916
     */
917 23
    public function setSiteHashFilter($allowedSites)
918
    {
919 23
        $allowedSites = GeneralUtility::trimExplode(',', $allowedSites);
920 23
        $filters = array();
921
922 23
        foreach ($allowedSites as $site) {
923 23
            $siteHash = Util::getSiteHashForDomain($site);
924
925 23
            $filters[] = 'siteHash:"' . $siteHash . '"';
926 23
        }
927
928 23
        $this->addFilter(implode(' OR ', $filters));
929 23
    }
930
931
    /**
932
     * Limits the query to certain page tree branches
933
     *
934
     * @param string $pageIds Comma-separated list of page IDs
935
     */
936
    public function setRootlineFilter($pageIds)
937
    {
938
        $pageIds = GeneralUtility::trimExplode(',', $pageIds);
939
        $filters = array();
940
941
        $processor = GeneralUtility::makeInstance('ApacheSolrForTypo3\\Solr\\FieldProcessor\\PageUidToHierarchy');
942
        $hierarchies = $processor->process($pageIds);
943
944
        foreach ($hierarchies as $hierarchy) {
945
            $lastLevel = array_pop($hierarchy);
946
            $filters[] = 'rootline:"' . $lastLevel . '"';
947
        }
948
949
        $this->addFilter(implode(' OR ', $filters));
950
    }
951
952
    /**
953
     * Adds a sort field and the sorting direction for that field
954
     *
955
     * @param string $fieldName The field name to sort by
956
     * @param string $direction Either ApacheSolrForTypo3\Solr\Query::SORT_ASC to sort the field ascending or ApacheSolrForTypo3\Solr\Query::SORT_DESC to sort descending
957
     * @return void
958
     * @throws \InvalidArgumentException if the $direction parameter given is neither ApacheSolrForTypo3\Solr\Query::SORT_ASC nor ApacheSolrForTypo3\Solr\Query::SORT_DESC
959
     */
960
    public function addSortField($fieldName, $direction)
961
    {
962
        switch ($direction) {
963
            case self::SORT_ASC:
964
            case self::SORT_DESC:
965
                $this->sortingFields[$fieldName] = $direction;
966
                break;
967
            default:
968
                throw new \InvalidArgumentException(
969
                    'Invalid sort direction "' . $direction . '"',
970
                    1235051723
971
                );
972
        }
973
    }
974
975
    /**
976
     * Gets the currently set sorting fields and their sorting directions
977
     *
978
     * @return array An associative array with the field names as key and their sorting direction as value
979
     */
980
    public function getSortingFields()
981
    {
982
        return $this->sortingFields;
983
    }
984
985
    /**
986
     * Gets the list of fields a query will return.
987
     *
988
     * @return array Array of field names the query will return
989
     */
990 2
    public function getFieldList()
991
    {
992 2
        return $this->fieldList;
993
    }
994
995
    /**
996
     * Sets the fields to return by a query.
997
     *
998
     * @param array|string $fieldList an array or comma-separated list of field names
999
     * @throws \UnexpectedValueException on parameters other than comma-separated lists and arrays
1000
     */
1001
    public function setFieldList($fieldList = array('*', 'score'))
1002
    {
1003
        if (is_string($fieldList)) {
1004
            $fieldList = GeneralUtility::trimExplode(',', $fieldList);
1005
        }
1006
1007
        if (!is_array($fieldList) || empty($fieldList)) {
1008
            throw new \UnexpectedValueException(
1009
                'Field list must be a comma-separated list or array and must not be empty.',
1010
                1310740308
1011
            );
1012
        }
1013
1014
        $this->fieldList = $fieldList;
1015
    }
1016
1017
    /**
1018
     * Gets the query type, Solr's qt parameter.
1019
     *
1020
     * @return string Query type, qt parameter.
1021
     */
1022
    public function getQueryType()
1023
    {
1024
        return $this->queryParameters['qt'];
1025
    }
1026
1027
    /**
1028
     * Sets the query type, Solr's qt parameter.
1029
     *
1030
     * @param string|bool $queryType String query type or boolean FALSE to disable / reset the qt parameter.
1031
     * @see http://wiki.apache.org/solr/CoreQueryParameters#qt
1032
     */
1033
    public function setQueryType($queryType)
1034
    {
1035
        if ($queryType) {
1036
            $this->queryParameters['qt'] = $queryType;
1037
        } else {
1038
            unset($this->queryParameters['qt']);
1039
        }
1040
    }
1041
1042
    /**
1043
     * Sets the query operator to AND or OR. Unsets the query operator (actually
1044
     * sets it back to default) for FALSE.
1045
     *
1046
     * @param string|bool $operator AND or OR, FALSE to unset
1047
     */
1048
    public function setOperator($operator)
1049
    {
1050
        if (in_array($operator, array(self::OPERATOR_AND, self::OPERATOR_OR))) {
1051
            $this->queryParameters['q.op'] = $operator;
1052
        }
1053
1054
        if ($operator === false) {
1055
            unset($this->queryParameters['q.op']);
1056
        }
1057
    }
1058
1059
    /**
1060
     * Gets the alternative query, Solr's q.alt parameter.
1061
     *
1062
     * @return string Alternative query, q.alt parameter.
1063
     */
1064
    public function getAlternativeQuery()
1065
    {
1066
        return $this->queryParameters['q.alt'];
1067
    }
1068
1069
    /**
1070
     * Sets an alternative query, Solr's q.alt parameter.
1071
     *
1072
     * This query supports the complete Lucene Query Language.
1073
     *
1074
     * @param mixed $alternativeQuery String alternative query or boolean FALSE to disable / reset the q.alt parameter.
1075
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#q.alt
1076
     */
1077 24
    public function setAlternativeQuery($alternativeQuery)
1078
    {
1079 24
        if ($alternativeQuery) {
1080 24
            $this->queryParameters['q.alt'] = $alternativeQuery;
1081 24
        } else {
1082
            unset($this->queryParameters['q.alt']);
1083
        }
1084 24
    }
1085
1086
    // keywords
1087
1088
    /**
1089
     * Set the query to omit the response header
1090
     *
1091
     * @param bool $omitHeader TRUE (default) to omit response headers, FALSE to re-enable
1092
     */
1093
    public function setOmitHeader($omitHeader = true)
1094
    {
1095
        if ($omitHeader) {
1096
            $this->queryParameters['omitHeader'] = 'true';
1097
        } else {
1098
            unset($this->queryParameters['omitHeader']);
1099
        }
1100
    }
1101
1102
    /**
1103
     * Get the query keywords, keywords are escaped.
1104
     *
1105
     * @return string query keywords
1106
     */
1107 20
    public function getKeywords()
1108
    {
1109 20
        return $this->keywords;
1110
    }
1111
1112
    /**
1113
     * Sets the query keywords, escapes them as needed for Solr/Lucene.
1114
     *
1115
     * @param string $keywords user search terms/keywords
1116
     */
1117 97
    public function setKeywords($keywords)
1118
    {
1119 97
        $this->keywords = $this->escape($keywords);
1120 97
        $this->keywordsRaw = $keywords;
1121 97
    }
1122
1123
    /**
1124
     * Gets the cleaned keywords so that it can be used in templates f.e.
1125
     *
1126
     * @return string The cleaned keywords.
1127
     */
1128 18
    public function getKeywordsCleaned()
1129
    {
1130 18
        return $this->cleanKeywords($this->keywordsRaw);
1131
    }
1132
1133
    /**
1134
     * Helper method to escape/encode keywords for use in HTML
1135
     *
1136
     * @param string $keywords Keywords to prepare for use in HTML
1137
     * @return string Encoded keywords
1138
     */
1139 22
    public static function cleanKeywords($keywords)
1140
    {
1141 22
        $keywords = trim($keywords);
1142 22
        $keywords = GeneralUtility::removeXSS($keywords);
1143 22
        $keywords = htmlentities($keywords, ENT_QUOTES,
1144 22
            $GLOBALS['TSFE']->metaCharset);
1145
1146
        // escape triple hashes as they are used in the template engine
1147
        // TODO remove after switching to fluid templates
1148 22
        $keywords = Template::escapeMarkers($keywords);
1149
1150 22
        return $keywords;
1151
    }
1152
1153
    // relevance, matching
1154
1155
    /**
1156
     * Gets the raw, unescaped, unencoded keywords.
1157
     *
1158
     * USE WITH CAUTION!
1159
     *
1160
     * @return string raw keywords
1161
     */
1162 19
    public function getKeywordsRaw()
1163
    {
1164 19
        return $this->keywordsRaw;
1165
    }
1166
1167
    /**
1168
     * Sets the minimum match (mm) parameter
1169
     *
1170
     * @param mixed $minimumMatch Minimum match parameter as string or boolean FALSE to disable / reset the mm parameter
1171
     * @see http://wiki.apache.org/solr/DisMaxRequestHandler#mm_.28Minimum_.27Should.27_Match.29
1172
     */
1173
    public function setMinimumMatch($minimumMatch)
1174
    {
1175
        if (is_string($minimumMatch) && !empty($minimumMatch)) {
1176
            $this->queryParameters['mm'] = $minimumMatch;
1177
        } else {
1178
            unset($this->queryParameters['mm']);
1179
        }
1180
    }
1181
1182
    /**
1183
     * Sets the boost function (bf) parameter
1184
     *
1185
     * @param mixed $boostFunction boost function parameter as string or boolean FALSE to disable / reset the bf parameter
1186
     * @see http://wiki.apache.org/solr/DisMaxRequestHandler#bf_.28Boost_Functions.29
1187
     */
1188
    public function setBoostFunction($boostFunction)
1189
    {
1190
        if (is_string($boostFunction) && !empty($boostFunction)) {
1191
            $this->queryParameters['bf'] = $boostFunction;
1192
        } else {
1193
            unset($this->queryParameters['bf']);
1194
        }
1195
    }
1196
1197
    // query fields
1198
    // TODO move up to field list methods
1199
1200
    /**
1201
     * Sets the boost query (bq) parameter
1202
     *
1203
     * @param mixed $boostQuery boost query parameter as string or array to set a boost query or boolean FALSE to disable / reset the bq parameter
1204
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#bq_.28Boost_Query.29
1205
     */
1206
    public function setBoostQuery($boostQuery)
1207
    {
1208
        if ((is_string($boostQuery) || is_array($boostQuery)) && !empty($boostQuery)) {
1209
            $this->queryParameters['bq'] = $boostQuery;
1210
        } else {
1211
            unset($this->queryParameters['bq']);
1212
        }
1213
    }
1214
1215
    /**
1216
     * Gets a specific query parameter by its name.
1217
     *
1218
     * @param string $parameterName The parameter to return
1219
     * @return string The parameter's value or NULL if not set
1220
     */
1221
    public function getQueryParameter($parameterName)
1222
    {
1223
        $requestedParameter = null;
1224
        $parameters = $this->getQueryParameters();
1225
1226
        if (isset($parameters[$parameterName])) {
1227
            $requestedParameter = $parameters[$parameterName];
1228
        }
1229
1230
        return $requestedParameter;
1231
    }
1232
1233
    /**
1234
     * Builds an array of query parameters to use for the search query.
1235
     *
1236
     * @return array An array ready to use with query parameters
1237
     */
1238 46
    public function getQueryParameters()
1239
    {
1240 46
        $queryParameters = array_merge(
1241
            array(
1242 46
                'fl' => implode(',', $this->fieldList),
1243 46
                'fq' => array_values($this->filters)
1244 46
            ),
1245 46
            $this->queryParameters
1246 46
        );
1247
1248 46
        $queryFieldString = $this->getQueryFieldsAsString();
1249 46
        if (!empty($queryFieldString)) {
1250 25
            $queryParameters['qf'] = $queryFieldString;
1251 25
        }
1252
1253 46
        return $queryParameters;
1254
    }
1255
1256
    // general query parameters
1257
1258
    /**
1259
     * Compiles the query fields into a string to be used in Solr's qf parameter.
1260
     *
1261
     * @return string A string of query fields with their associated boosts
1262
     */
1263 48
    public function getQueryFieldsAsString()
1264
    {
1265 48
        $queryFieldString = '';
1266
1267 48
        foreach ($this->queryFields as $fieldName => $fieldBoost) {
1268 26
            $queryFieldString .= $fieldName;
1269
1270 26
            if ($fieldBoost != 1.0) {
1271 26
                $queryFieldString .= '^' . number_format($fieldBoost, 1, '.', '');
1272 26
            }
1273
1274 26
            $queryFieldString .= ' ';
1275 48
        }
1276
1277 48
        return trim($queryFieldString);
1278
    }
1279
1280
    /**
1281
     * Enables or disables highlighting of search terms in result teasers.
1282
     *
1283
     * @param bool $highlighting Enables highlighting when set to TRUE, deactivates highlighting when set to FALSE, defaults to TRUE.
1284
     * @param int $fragmentSize Size, in characters, of fragments to consider for highlighting.
1285
     * @see http://wiki.apache.org/solr/HighlightingParameters
1286
     * @return void
1287
     */
1288 29
    public function setHighlighting($highlighting = true, $fragmentSize = 200)
1289
    {
1290 29
        if ($highlighting) {
1291 29
            $this->queryParameters['hl'] = 'true';
1292 29
            $this->queryParameters['hl.fragsize'] = (int)$fragmentSize;
1293
1294 29
            $highlightingFields = $this->solrConfiguration->getSearchResultsHighlightingFields();
1295 29
            if ($highlightingFields != '') {
1296 24
                $this->queryParameters['hl.fl'] = $highlightingFields;
1297 24
            }
1298
1299
            // the fast vector highlighter can only be used, when the fragmentSize is
1300
            // higher then 17 otherwise solr throws an exception
1301 29
            $useFastVectorHighlighter = ($fragmentSize >= 18);
1302 29
            $wrap = explode('|', $this->solrConfiguration->getSearchResultsHighlightingWrap());
1303
1304 29
            if ($useFastVectorHighlighter) {
1305 27
                $this->queryParameters['hl.useFastVectorHighlighter'] = 'true';
1306 27
                $this->queryParameters['hl.tag.pre'] = $wrap[0];
1307 27
                $this->queryParameters['hl.tag.post'] = $wrap[1];
1308 27
            }
1309
1310 29
            if (isset($wrap[0]) && isset($wrap[1])) {
1311 25
                $this->queryParameters['hl.simple.pre'] = $wrap[0];
1312 25
                $this->queryParameters['hl.simple.post'] = $wrap[1];
1313 25
            }
1314 29
        } else {
1315
            // remove all hl.* settings
1316
            foreach ($this->queryParameters as $key => $value) {
1317
                if (GeneralUtility::isFirstPartOfStr($key, 'hl')) {
1318
                    unset($this->queryParameters[$key]);
1319
                }
1320
            }
1321
        }
1322 29
    }
1323
1324
    // misc
1325
1326
    /**
1327
     * Enables or disables spellchecking for the query.
1328
     *
1329
     * @param bool $spellchecking Enables spellchecking when set to TRUE, deactivates spellchecking when set to FALSE, defaults to TRUE.
1330
     */
1331 22
    public function setSpellchecking($spellchecking = true)
1332
    {
1333 22
        if ($spellchecking) {
1334 22
            $this->queryParameters['spellcheck'] = 'true';
1335 22
            $this->queryParameters['spellcheck.collate'] = 'true';
1336 22
            $maxCollationTries = $this->solrConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry();
1337 22
            $this->addQueryParameter('spellcheck.maxCollationTries', $maxCollationTries);
1338 22
        } else {
1339
            unset($this->queryParameters['spellcheck']);
1340
            unset($this->queryParameters['spellcheck.collate']);
1341
            unset($this->queryParameters['spellcheck.maxCollationTries']);
1342
        }
1343 22
    }
1344
1345
    /**
1346
     * Adds any generic query parameter.
1347
     *
1348
     * @param string $parameterName Query parameter name
1349
     * @param string $parameterValue Parameter value
1350
     */
1351 29
    public function addQueryParameter($parameterName, $parameterValue)
1352
    {
1353 29
        $this->queryParameters[$parameterName] = $parameterValue;
1354 29
    }
1355
1356
    /**
1357
     * Sets the sort parameter.
1358
     *
1359
     * $sorting must include a field name (or the pseudo-field score),
1360
     * followed by a space,
1361
     * followed by a sort direction (asc or desc).
1362
     *
1363
     * Multiple fallback sortings can be separated by comma,
1364
     * ie: <field name> <direction>[,<field name> <direction>]...
1365
     *
1366
     * @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)
1367
     * @see http://wiki.apache.org/solr/CommonQueryParameters#sort
1368
     */
1369 1
    public function setSorting($sorting)
1370
    {
1371 1
        if ($sorting) {
1372 1
            $sortParameter = $sorting;
1373
1374 1
            list($sortField) = explode(' ', $sorting);
1375 1
            if ($sortField == 'relevance') {
1376
                $sortParameter = '';
1377
            }
1378
1379 1
            $this->queryParameters['sort'] = $sortParameter;
1380 1
        } else {
1381
            unset($this->queryParameters['sort']);
1382
        }
1383 1
    }
1384
1385
    /**
1386
     * Enables or disables the debug parameter for the query.
1387
     *
1388
     * @param bool $debugMode Enables debugging when set to TRUE, deactivates debugging when set to FALSE, defaults to TRUE.
1389
     */
1390 20
    public function setDebugMode($debugMode = true)
1391
    {
1392 20
        if ($debugMode) {
1393 20
            $this->queryParameters['debugQuery'] = 'true';
1394 20
            $this->queryParameters['echoParams'] = 'all';
1395 20
        } else {
1396
            unset($this->queryParameters['debugQuery']);
1397
            unset($this->queryParameters['echoParams']);
1398
        }
1399 20
    }
1400
1401
    /**
1402
     * Returns the link target page id.
1403
     *
1404
     * @return int
1405
     */
1406 2
    public function getLinkTargetPageId()
1407
    {
1408 2
        return $this->linkTargetPageId;
1409
    }
1410
1411
    /**
1412
     * Activates the collapsing on the configured field, if collapsing was enabled.
1413
     *
1414
     * @return bool
1415
     */
1416 96
    protected function initializeCollapsingFromConfiguration()
1417
    {
1418
        // check collapsing
1419 96
        if ($this->solrConfiguration->getSearchVariants()) {
1420 2
            $collapseField = $this->solrConfiguration->getSearchVariantsField();
1421 2
            $this->setVariantField($collapseField);
1422 2
            $this->setCollapsing(true);
1423
1424 2
            return true;
1425
        }
1426
1427 94
        return false;
1428
    }
1429
}
1430