Completed
Push — master ( 043dac...30f7bc )
by Timo
07:40
created

Query::initializeQuery()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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