Completed
Branch master (18b261)
by Timo
28:03 queued 21:53
created

Query::setForceElevation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 5
cts 5
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 2
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 127
    public function __construct($keywords, $solrConfiguration = null)
157
    {
158 127
        $keywords = (string) $keywords;
159 127
        if ($solrConfiguration == null) {
160 76
            $this->solrConfiguration = Util::getSolrConfiguration();
161
        } else {
162 51
            $this->solrConfiguration = $solrConfiguration;
163
        }
164
165 127
        $this->setKeywords($keywords);
166 127
        $this->sorting = '';
167
168
        // What fields to search
169 127
        $queryFields = $this->solrConfiguration->getSearchQueryQueryFields();
170 127
        if ($queryFields != '') {
171 25
            $this->setQueryFieldsFromString($queryFields);
172
        }
173
174
        // What fields to return from Solr
175 127
        $this->fieldList = $this->solrConfiguration->getSearchQueryReturnFieldsAsArray(array('*', 'score'));
176 127
        $this->linkTargetPageId = $this->solrConfiguration->getSearchTargetPage();
177
178 127
        $this->initializeQuery();
179
180 127
        $this->id = ++self::$idCount;
181 127
    }
182
183
    /**
184
     * @return void
185
     */
186 126
    protected function initializeQuery()
187
    {
188 126
        $this->initializeCollapsingFromConfiguration();
189 126
    }
190
191
    /**
192
     * Writes a message to the devLog.
193
     *
194
     * @param string $msg
195
     * @param int $severity
196
     * @param mixed $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
            }
227
228 26
            $this->setQueryField($fieldNameAndBoost[0], $boost);
229
        }
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 5
    public function __clone()
253
    {
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 8
    public function __toString()
263
    {
264 8
        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 36
    public function getQueryString()
273
    {
274 36
        if (!$this->rawQueryString) {
275 34
            $this->buildQueryString();
276
        }
277
278 36
        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 3
    public function setQueryString($queryString)
290
    {
291 3
        $this->queryString = $queryString;
292 3
    }
293
294
    /**
295
     * Creates the string that is later used as the q parameter in the solr query
296
     *
297
     * @return void
298
     */
299 34
    protected function buildQueryString()
300
    {
301
        // very simple for now
302 34
        $this->queryString = $this->keywords;
303 34
    }
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 3
    public function useRawQueryString($useRawQueryString)
312
    {
313 3
        $this->rawQueryString = (boolean)$useRawQueryString;
314 3
    }
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 127
    public function escape($string)
333
    {
334
        // when we have a numeric string only, nothing needs to be done
335 127
        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 127
        if (preg_match('/\W/', $string) != 1) {
341 106
            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 37
        $hasQuotes = strrpos($string, '"') !== false;
346 37
        if (!$hasQuotes) {
347 30
            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
            }
379
380 7
            if ($isInQuote && !$isLastQuote) {
381 6
                $result .= $this->escapePhrase($segment);
382
            } else {
383 7
                $result .= $this->escapeSpecialCharacters($segment);
384
            }
385
386 7
            $segmentsIndex++;
387
        }
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 127
    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 127
        $pattern = '/(\\(|\\)|\\{|\\}|\\[|\\]|\\^|"|~|\:|\\\\|\\/)/';
422 127
        $replace = '\\\$1';
423
424 127
        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 1
    public function getPage()
433
    {
434 1
        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 1
    public function setPage($page)
444
    {
445 1
        $this->page = max(intval($page), 0);
446 1
    }
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 25
    public function setQueryElevation(
479
        $elevation = true,
480
        $forceElevation = true,
481
        $markElevatedResults = true
482
    ) {
483 25
        if ($elevation) {
484 22
            $this->queryParameters['enableElevation'] = 'true';
485 22
            $this->setForceElevation($forceElevation);
486 22
            if ($markElevatedResults) {
487 22
                $this->addReturnField('isElevated:[elevated]');
488
            }
489
        } else {
490 4
            $this->queryParameters['enableElevation'] = 'false';
491 4
            unset($this->queryParameters['forceElevation']);
492 4
            $this->removeReturnField('isElevated:[elevated]');
493 4
            $this->removeReturnField('[elevated]'); // fallback
494
        }
495 25
    }
496
497
    /**
498
     * Enables or disables the forceElevation query parameter.
499
     *
500
     * @param boolean $forceElevation
501
     */
502 22
    protected function setForceElevation($forceElevation)
503
    {
504 22
        if ($forceElevation) {
505 21
            $this->queryParameters['forceElevation'] = 'true';
506
        } else {
507 1
            $this->queryParameters['forceElevation'] = 'false';
508
        }
509 22
    }
510
511
    // collapsing
512
513
    /**
514
     * Check whether collapsing is active
515
     *
516
     * @return bool
517
     */
518 3
    public function getIsCollapsing()
519
    {
520 3
        return array_key_exists('collapsing', $this->filters);
521
    }
522
523
    /**
524
     * @param string $fieldName
525
     */
526 2
    public function setVariantField($fieldName)
527
    {
528 2
        $this->variantField = $fieldName;
529 2
    }
530
531
    /**
532
     * @return string
533
     */
534 1
    public function getVariantField()
535
    {
536 1
        return $this->variantField;
537
    }
538
539
    /**
540
     * @param bool $collapsing
541
     */
542 3
    public function setCollapsing($collapsing = true)
543
    {
544 3
        if ($collapsing) {
545 3
            $this->filters['collapsing'] = '{!collapse field=' . $this->variantField . '}';
546 3
            if ($this->solrConfiguration->getSearchVariantsExpand()) {
547 1
                $this->queryParameters['expand'] = 'true';
548 3
                $this->queryParameters['expand.rows'] = $this->solrConfiguration->getSearchVariantsLimit();
549
            }
550
        } else {
551 1
            unset($this->filters['collapsing']);
552 1
            unset($this->queryParameters['expand']);
553 1
            unset($this->queryParameters['expand.rows']);
554
        }
555 3
    }
556
557
    // grouping
558
559
    /**
560
     * Adds a field to the list of fields to return. Also checks whether * is
561
     * set for the fields, if so it's removed from the field list.
562
     *
563
     * @param string $fieldName Name of a field to return in the result documents
564
     */
565 23
    public function addReturnField($fieldName)
566
    {
567 23
        if (strpos($fieldName, '[') === false
568 23
            && strpos($fieldName, ']') === false
569 23
            && in_array('*', $this->fieldList)
570
        ) {
571 1
            $this->fieldList = array_diff($this->fieldList, array('*'));
572
        }
573
574 23
        $this->fieldList[] = $fieldName;
575 23
    }
576
577
    /**
578
     * Removes a field from the list of fields to return (fl parameter).
579
     *
580
     * @param string $fieldName Field to remove from the list of fields to return
581
     */
582 5
    public function removeReturnField($fieldName)
583
    {
584 5
        $key = array_search($fieldName, $this->fieldList);
585
586 5
        if ($key !== false) {
587 2
            unset($this->fieldList[$key]);
588
        }
589 5
    }
590
591
    /**
592
     * Activates and deactivates grouping for the current query.
593
     *
594
     * @param bool $grouping TRUE to enable grouping, FALSE to disable grouping
595
     * @return void
596
     */
597 2
    public function setGrouping($grouping = true)
598
    {
599 2
        if ($grouping) {
600 1
            $this->queryParameters['group'] = 'true';
601 1
            $this->queryParameters['group.format'] = 'grouped';
602 1
            $this->queryParameters['group.ngroups'] = 'true';
603
        } else {
604 1
            foreach ($this->queryParameters as $key => $value) {
605
                // remove all group.* settings
606 1
                if (GeneralUtility::isFirstPartOfStr($key, 'group')) {
607 1
                    unset($this->queryParameters[$key]);
608
                }
609
            }
610
        }
611 2
    }
612
613
    /**
614
     * Sets the number of groups to return per group field or group query
615
     *
616
     * Internally uses the rows parameter.
617
     *
618
     * @param int $numberOfGroups Number of groups per group.field or group.query
619
     */
620 1
    public function setNumberOfGroups($numberOfGroups)
621
    {
622 1
        $this->setResultsPerPage($numberOfGroups);
623 1
    }
624
625
    /**
626
     * Gets the number of groups to return per group field or group query
627
     *
628
     * Internally uses the rows parameter.
629
     *
630
     * @return int Number of groups per group.field or group.query
631
     */
632 1
    public function getNumberOfGroups()
633
    {
634 1
        return $this->getResultsPerPage();
635
    }
636
637
    /**
638
     * Returns the number of results that should be shown per page
639
     *
640
     * @return int number of results to show per page
641
     */
642 24
    public function getResultsPerPage()
643
    {
644 24
        return $this->resultsPerPage;
645
    }
646
647
    /**
648
     * Sets the number of results that should be shown per page
649
     *
650
     * @param int $resultsPerPage Number of results to show per page
651
     * @return void
652
     */
653 31
    public function setResultsPerPage($resultsPerPage)
654
    {
655 31
        $this->resultsPerPage = max(intval($resultsPerPage), 0);
656 31
    }
657
658
    /**
659
     * Adds a field that should be used for grouping.
660
     *
661
     * @param string $fieldName Name of a field for grouping
662
     */
663 1
    public function addGroupField($fieldName)
664
    {
665 1
        if (!isset($this->queryParameters['group.field'])) {
666 1
            $this->queryParameters['group.field'] = array();
667
        }
668
669 1
        $this->queryParameters['group.field'][] = $fieldName;
670 1
    }
671
672
    /**
673
     * Gets the fields set for grouping.
674
     *
675
     * @return array An array of fields set for grouping.
676
     */
677 1
    public function getGroupFields()
678
    {
679 1
        return (array) $this->getQueryParameter('group.field', []);
680
    }
681
682
    /**
683
     * Adds sorting configuration for grouping.
684
     *
685
     * @param string $sorting value of sorting configuration
686
     */
687 1
    public function addGroupSorting($sorting)
688
    {
689 1
        if (!isset($this->queryParameters['group.sort'])) {
690 1
            $this->queryParameters['group.sort'] = array();
691
        }
692 1
        $this->queryParameters['group.sort'][] = $sorting;
693 1
    }
694
695
    /**
696
     * Gets the sorting set for grouping.
697
     *
698
     * @return array An array of sorting configurations for grouping.
699
     */
700 1
    public function getGroupSortings()
701
    {
702 1
        return (array) $this->getQueryParameter('group.sort', []);
703
    }
704
705
    // faceting
706
707
    /**
708
     * Adds a query that should be used for grouping.
709
     *
710
     * @param string $query Lucene query for grouping
711
     */
712 1
    public function addGroupQuery($query)
713
    {
714 1
        if (!isset($this->queryParameters['group.query'])) {
715 1
            $this->queryParameters['group.query'] = array();
716
        }
717
718 1
        $this->queryParameters['group.query'][] = $query;
719 1
    }
720
721
    /**
722
     * Gets the queries set for grouping.
723
     *
724
     * @return array An array of queries set for grouping.
725
     */
726 1
    public function getGroupQueries()
727
    {
728 1
        return (array) $this->getQueryParameter('group.query', []);
729
    }
730
731
    /**
732
     * Sets the maximum number of results to be returned per group.
733
     *
734
     * @param int $numberOfResults Maximum number of results per group to return
735
     */
736 1
    public function setNumberOfResultsPerGroup($numberOfResults)
737
    {
738 1
        $numberOfResults = max(intval($numberOfResults), 0);
739
740 1
        $this->queryParameters['group.limit'] = $numberOfResults;
741 1
    }
742
743
    // filter
744
745
    /**
746
     * Gets the maximum number of results to be returned per group.
747
     *
748
     * @return int Maximum number of results per group to return
749
     */
750 1
    public function getNumberOfResultsPerGroup()
751
    {
752
        // default if nothing else set is 1, @see http://wiki.apache.org/solr/FieldCollapsing
753 1
        $numberOfResultsPerGroup = 1;
754
755 1
        if (!empty($this->queryParameters['group.limit'])) {
756 1
            $numberOfResultsPerGroup = $this->queryParameters['group.limit'];
757
        }
758
759 1
        return $numberOfResultsPerGroup;
760
    }
761
762
    /**
763
     * Activates and deactivates faceting for the current query.
764
     *
765
     * @param bool $faceting TRUE to enable faceting, FALSE to disable faceting
766
     * @return void
767
     */
768 31
    public function setFaceting($faceting = true)
769
    {
770 31
        if ($faceting) {
771 31
            $this->queryParameters['facet'] = 'true';
772 31
            $this->queryParameters['facet.mincount'] = $this->solrConfiguration->getSearchFacetingMinimumCount();
773
774 31
            $this->applyConfiguredFacetSorting();
775
        } else {
776 1
            $this->removeFacetingParametersFromQuery();
777
        }
778 31
    }
779
780
    /**
781
     * Removes all facet.* or f.*.facet.* parameters from the query.
782
     *
783
     * @return void
784
     */
785 1
    protected function removeFacetingParametersFromQuery()
786
    {
787 1
        foreach ($this->queryParameters as $key => $value) {
788
            // remove all facet.* settings
789 1
            if (GeneralUtility::isFirstPartOfStr($key, 'facet')) {
790 1
                unset($this->queryParameters[$key]);
791
            }
792
793
            // remove all f.*.facet.* settings (overrides for individual fields)
794 1
            if (GeneralUtility::isFirstPartOfStr($key, 'f.') && strpos($key, '.facet.') !== false) {
795 1
                unset($this->queryParameters[$key]);
796
            }
797
        }
798 1
    }
799
800
    /**
801
     * Reads the facet sorting configuration and applies it to the queryParameters.
802
     *
803
     * @return void
804
     */
805 31
    protected function applyConfiguredFacetSorting()
806
    {
807 31
        $sorting = $this->solrConfiguration->getSearchFacetingSortBy();
808 31
        if (!GeneralUtility::inList('count,index,alpha,lex,1,0,true,false', $sorting)) {
809
            // when the sorting is not in the list of valid values we do not apply it.
810 10
            return;
811
        }
812
813
        // alpha and lex alias for index
814 21
        if ($sorting == 'alpha' || $sorting == 'lex') {
815 1
            $sorting = 'index';
816
        }
817
818 21
        $this->queryParameters['facet.sort'] = $sorting;
819 21
    }
820
821
    /**
822
     * Sets facet fields for a query.
823
     *
824
     * @param array $facetFields Array of field names
825
     */
826 1
    public function setFacetFields(array $facetFields)
827
    {
828 1
        $this->queryParameters['facet.field'] = array();
829
830 1
        foreach ($facetFields as $facetField) {
831 1
            $this->addFacetField($facetField);
832
        }
833 1
    }
834
835
    /**
836
     * Adds a single facet field.
837
     *
838
     * @param string $facetField field name
839
     */
840 2
    public function addFacetField($facetField)
841
    {
842 2
        $this->queryParameters['facet.field'][] = $facetField;
843 2
    }
844
845
    /**
846
     * Removes a filter on a field
847
     *
848
     * @param string $filterFieldName The field name the filter should be removed for
849
     * @return void
850
     */
851 1
    public function removeFilter($filterFieldName)
852
    {
853 1
        foreach ($this->filters as $key => $filterString) {
854 1
            if (GeneralUtility::isFirstPartOfStr($filterString,
855 1
                $filterFieldName . ':')
856
            ) {
857 1
                unset($this->filters[$key]);
858
            }
859
        }
860 1
    }
861
862
    /**
863
     * Removes a filter based on key of filter array
864
     *
865
     * @param string $key array key
866
     */
867 1
    public function removeFilterByKey($key)
868
    {
869 1
        unset($this->filters[$key]);
870 1
    }
871
872
    /**
873
     * Gets all currently applied filters.
874
     *
875
     * @return array Array of filters
876
     */
877 27
    public function getFilters()
878
    {
879 27
        return $this->filters;
880
    }
881
882
    // sorting
883
884
    /**
885
     * Sets access restrictions for a frontend user.
886
     *
887
     * @param array $groups Array of groups a user has been assigned to
888
     */
889 28
    public function setUserAccessGroups(array $groups)
890
    {
891 28
        $groups = array_map('intval', $groups);
892 28
        $groups[] = 0; // always grant access to public documents
893 28
        $groups = array_unique($groups);
894 28
        sort($groups, SORT_NUMERIC);
895
896 28
        $accessFilter = '{!typo3access}' . implode(',', $groups);
897
898 28
        foreach ($this->filters as $key => $filter) {
899 24
            if (GeneralUtility::isFirstPartOfStr($filter, '{!typo3access}')) {
900 24
                unset($this->filters[$key]);
901
            }
902
        }
903
904 28
        $this->addFilter($accessFilter);
905 28
    }
906
907
    /**
908
     * Adds a filter parameter.
909
     *
910
     * @param string $filterString The filter to add, in the form of field:value
911
     * @return void
912
     */
913 34
    public function addFilter($filterString)
914
    {
915
        // TODO refactor to split filter field and filter value, @see Drupal
916 34
        if ($this->solrConfiguration->getLoggingQueryFilters()) {
917 1
            $this->writeDevLog('adding filter', 0, array($filterString));
918
        }
919
920 34
        $this->filters[] = $filterString;
921 34
    }
922
923
    // query parameters
924
925
    /**
926
     * Limits the query to certain sites
927
     *
928
     * @param string $allowedSites Comma-separated list of domains
929
     */
930 23
    public function setSiteHashFilter($allowedSites)
931
    {
932 23
        $allowedSites = GeneralUtility::trimExplode(',', $allowedSites);
933 23
        $filters = array();
934
935 23
        foreach ($allowedSites as $site) {
936 23
            $siteHash = Util::getSiteHashForDomain($site);
937
938 23
            $filters[] = 'siteHash:"' . $siteHash . '"';
939
        }
940
941 23
        $this->addFilter(implode(' OR ', $filters));
942 23
    }
943
944
    /**
945
     * Limits the query to certain page tree branches
946
     *
947
     * @param string $pageIds Comma-separated list of page IDs
948
     */
949
    public function setRootlineFilter($pageIds)
950
    {
951
        $pageIds = GeneralUtility::trimExplode(',', $pageIds);
952
        $filters = array();
953
954
        $processor = GeneralUtility::makeInstance('ApacheSolrForTypo3\\Solr\\FieldProcessor\\PageUidToHierarchy');
955
        $hierarchies = $processor->process($pageIds);
956
957
        foreach ($hierarchies as $hierarchy) {
958
            $lastLevel = array_pop($hierarchy);
959
            $filters[] = 'rootline:"' . $lastLevel . '"';
960
        }
961
962
        $this->addFilter(implode(' OR ', $filters));
963
    }
964
965
    /**
966
     * Adds a sort field and the sorting direction for that field
967
     *
968
     * @param string $fieldName The field name to sort by
969
     * @param string $direction Either ApacheSolrForTypo3\Solr\Query::SORT_ASC to sort the field ascending or ApacheSolrForTypo3\Solr\Query::SORT_DESC to sort descending
970
     * @return void
971
     * @throws \InvalidArgumentException if the $direction parameter given is neither ApacheSolrForTypo3\Solr\Query::SORT_ASC nor ApacheSolrForTypo3\Solr\Query::SORT_DESC
972
     */
973 1
    public function addSortField($fieldName, $direction)
974
    {
975 1
        $isValidSorting = $direction === self::SORT_DESC || $direction === self::SORT_ASC;
976 1
        if (!$isValidSorting) {
977 1
            throw new \InvalidArgumentException('Invalid sort direction "' . $direction . '"', 1235051723);
978
        }
979
980 1
        $this->sortingFields[$fieldName] = $direction;
981 1
    }
982
983
    /**
984
     * Gets the currently set sorting fields and their sorting directions
985
     *
986
     * @return array An associative array with the field names as key and their sorting direction as value
987
     */
988 1
    public function getSortingFields()
989
    {
990 1
        return $this->sortingFields;
991
    }
992
993
    /**
994
     * Gets the list of fields a query will return.
995
     *
996
     * @return array Array of field names the query will return
997
     */
998 6
    public function getFieldList()
999
    {
1000 6
        return $this->fieldList;
1001
    }
1002
1003
    /**
1004
     * Sets the fields to return by a query.
1005
     *
1006
     * @param array|string $fieldList an array or comma-separated list of field names
1007
     * @throws \UnexpectedValueException on parameters other than comma-separated lists and arrays
1008
     */
1009 2
    public function setFieldList($fieldList = array('*', 'score'))
1010
    {
1011 2
        if (is_string($fieldList)) {
1012 1
            $fieldList = GeneralUtility::trimExplode(',', $fieldList);
1013
        }
1014
1015 2
        if (!is_array($fieldList) || empty($fieldList)) {
1016 1
            throw new \UnexpectedValueException(
1017 1
                'Field list must be a comma-separated list or array and must not be empty.',
1018 1
                1310740308
1019
            );
1020
        }
1021
1022 2
        $this->fieldList = $fieldList;
1023 2
    }
1024
1025
    /**
1026
     * Gets the query type, Solr's qt parameter.
1027
     *
1028
     * @return string Query type, qt parameter.
1029
     */
1030 1
    public function getQueryType()
1031
    {
1032 1
        return $this->queryParameters['qt'];
1033
    }
1034
1035
    /**
1036
     * Sets the query type, Solr's qt parameter.
1037
     *
1038
     * @param string|bool $queryType String query type or boolean FALSE to disable / reset the qt parameter.
1039
     * @see http://wiki.apache.org/solr/CoreQueryParameters#qt
1040
     */
1041 1
    public function setQueryType($queryType)
1042
    {
1043 1
        if ($queryType) {
1044 1
            $this->queryParameters['qt'] = $queryType;
1045
        } else {
1046 1
            unset($this->queryParameters['qt']);
1047
        }
1048 1
    }
1049
1050
    /**
1051
     * Sets the query operator to AND or OR. Unsets the query operator (actually
1052
     * sets it back to default) for FALSE.
1053
     *
1054
     * @param string|bool $operator AND or OR, FALSE to unset
1055
     */
1056 1
    public function setOperator($operator)
1057
    {
1058 1
        if (in_array($operator, array(self::OPERATOR_AND, self::OPERATOR_OR))) {
1059 1
            $this->queryParameters['q.op'] = $operator;
1060
        }
1061
1062 1
        if ($operator === false) {
1063 1
            unset($this->queryParameters['q.op']);
1064
        }
1065 1
    }
1066
1067
    /**
1068
     * Gets the alternative query, Solr's q.alt parameter.
1069
     *
1070
     * @return string Alternative query, q.alt parameter.
1071
     */
1072 1
    public function getAlternativeQuery()
1073
    {
1074 1
        return $this->queryParameters['q.alt'];
1075
    }
1076
1077
    /**
1078
     * Sets an alternative query, Solr's q.alt parameter.
1079
     *
1080
     * This query supports the complete Lucene Query Language.
1081
     *
1082
     * @param mixed $alternativeQuery String alternative query or boolean FALSE to disable / reset the q.alt parameter.
1083
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#q.alt
1084
     */
1085 25
    public function setAlternativeQuery($alternativeQuery)
1086
    {
1087 25
        if ($alternativeQuery) {
1088 25
            $this->queryParameters['q.alt'] = $alternativeQuery;
1089
        } else {
1090 1
            unset($this->queryParameters['q.alt']);
1091
        }
1092 25
    }
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 1
    public function setOmitHeader($omitHeader = true)
1102
    {
1103 1
        if ($omitHeader) {
1104 1
            $this->queryParameters['omitHeader'] = 'true';
1105
        } else {
1106 1
            unset($this->queryParameters['omitHeader']);
1107
        }
1108 1
    }
1109
1110
    /**
1111
     * Get the query keywords, keywords are escaped.
1112
     *
1113
     * @return string query keywords
1114
     */
1115 20
    public function getKeywords()
1116
    {
1117 20
        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 127
    public function setKeywords($keywords)
1126
    {
1127 127
        $this->keywords = $this->escape($keywords);
1128 127
        $this->keywordsRaw = $keywords;
1129 127
    }
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 18
    public function getKeywordsCleaned()
1137
    {
1138 18
        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 22
    public static function cleanKeywords($keywords)
1148
    {
1149 22
        $keywords = trim($keywords);
1150 22
        $keywords = GeneralUtility::removeXSS($keywords);
1151 22
        $keywords = htmlentities($keywords, ENT_QUOTES,
1152 22
            $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 22
        $keywords = Template::escapeMarkers($keywords);
1157
1158 22
        return $keywords;
1159
    }
1160
1161
    // relevance, matching
1162
1163
    /**
1164
     * Gets the raw, unescaped, unencoded keywords.
1165
     *
1166
     * USE WITH CAUTION!
1167
     *
1168
     * @return string raw keywords
1169
     */
1170 19
    public function getKeywordsRaw()
1171
    {
1172 19
        return $this->keywordsRaw;
1173
    }
1174
1175
    /**
1176
     * Sets the minimum match (mm) parameter
1177
     *
1178
     * @param mixed $minimumMatch Minimum match parameter as string or boolean FALSE to disable / reset the mm parameter
1179
     * @see http://wiki.apache.org/solr/DisMaxRequestHandler#mm_.28Minimum_.27Should.27_Match.29
1180
     */
1181 1
    public function setMinimumMatch($minimumMatch)
1182
    {
1183 1
        if (is_string($minimumMatch) && !empty($minimumMatch)) {
1184 1
            $this->queryParameters['mm'] = $minimumMatch;
1185
        } else {
1186 1
            unset($this->queryParameters['mm']);
1187
        }
1188 1
    }
1189
1190
    /**
1191
     * Sets the boost function (bf) parameter
1192
     *
1193
     * @param mixed $boostFunction boost function parameter as string or boolean FALSE to disable / reset the bf parameter
1194
     * @see http://wiki.apache.org/solr/DisMaxRequestHandler#bf_.28Boost_Functions.29
1195
     */
1196 1
    public function setBoostFunction($boostFunction)
1197
    {
1198 1
        if (is_string($boostFunction) && !empty($boostFunction)) {
1199 1
            $this->queryParameters['bf'] = $boostFunction;
1200
        } else {
1201 1
            unset($this->queryParameters['bf']);
1202
        }
1203 1
    }
1204
1205
    // query fields
1206
    // TODO move up to field list methods
1207
1208
    /**
1209
     * Sets the boost query (bq) parameter
1210
     *
1211
     * @param mixed $boostQuery boost query parameter as string or array to set a boost query or boolean FALSE to disable / reset the bq parameter
1212
     * @see http://wiki.apache.org/solr/DisMaxQParserPlugin#bq_.28Boost_Query.29
1213
     */
1214 1
    public function setBoostQuery($boostQuery)
1215
    {
1216 1
        if ((is_string($boostQuery) || is_array($boostQuery)) && !empty($boostQuery)) {
1217 1
            $this->queryParameters['bq'] = $boostQuery;
1218
        } else {
1219 1
            unset($this->queryParameters['bq']);
1220
        }
1221 1
    }
1222
1223
    /**
1224
     * Gets a specific query parameter by its name.
1225
     *
1226
     * @param string $parameterName The parameter to return
1227
     * @param mixed $defaultIfEmpty
1228
     * @return mixed The parameter's value or $defaultIfEmpty if not set
1229
     */
1230 10
    public function getQueryParameter($parameterName, $defaultIfEmpty = null)
1231
    {
1232 10
        $parameters = $this->getQueryParameters();
1233 10
        return isset($parameters[$parameterName]) ? $parameters[$parameterName] : $defaultIfEmpty;
1234
    }
1235
1236
    /**
1237
     * Builds an array of query parameters to use for the search query.
1238
     *
1239
     * @return array An array ready to use with query parameters
1240
     */
1241 62
    public function getQueryParameters()
1242
    {
1243 62
        $queryParameters = array_merge(
1244
            array(
1245 62
                'fl' => implode(',', $this->fieldList),
1246 62
                'fq' => array_values($this->filters)
1247
            ),
1248 62
            $this->queryParameters
1249
        );
1250
1251 62
        $queryFieldString = $this->getQueryFieldsAsString();
1252 62
        if (!empty($queryFieldString)) {
1253 25
            $queryParameters['qf'] = $queryFieldString;
1254
        }
1255
1256 62
        return $queryParameters;
1257
    }
1258
1259
    // general query parameters
1260
1261
    /**
1262
     * Compiles the query fields into a string to be used in Solr's qf parameter.
1263
     *
1264
     * @return string A string of query fields with their associated boosts
1265
     */
1266 64
    public function getQueryFieldsAsString()
1267
    {
1268 64
        $queryFieldString = '';
1269
1270 64
        foreach ($this->queryFields as $fieldName => $fieldBoost) {
1271 26
            $queryFieldString .= $fieldName;
1272
1273 26
            if ($fieldBoost != 1.0) {
1274 26
                $queryFieldString .= '^' . number_format($fieldBoost, 1, '.', '');
1275
            }
1276
1277 26
            $queryFieldString .= ' ';
1278
        }
1279
1280 64
        return trim($queryFieldString);
1281
    }
1282
1283
    /**
1284
     * Enables or disables highlighting of search terms in result teasers.
1285
     *
1286
     * @param bool $highlighting Enables highlighting when set to TRUE, deactivates highlighting when set to FALSE, defaults to TRUE.
1287
     * @param int $fragmentSize Size, in characters, of fragments to consider for highlighting.
1288
     * @see http://wiki.apache.org/solr/HighlightingParameters
1289
     * @return void
1290
     */
1291 30
    public function setHighlighting($highlighting = true, $fragmentSize = 200)
1292
    {
1293 30
        if ($highlighting) {
1294 30
            $this->queryParameters['hl'] = 'true';
1295 30
            $this->queryParameters['hl.fragsize'] = (int)$fragmentSize;
1296
1297 30
            $highlightingFields = $this->solrConfiguration->getSearchResultsHighlightingFields();
1298 30
            if ($highlightingFields != '') {
1299 24
                $this->queryParameters['hl.fl'] = $highlightingFields;
1300
            }
1301
1302
            // the fast vector highlighter can only be used, when the fragmentSize is
1303
            // higher then 17 otherwise solr throws an exception
1304 30
            $useFastVectorHighlighter = ($fragmentSize >= 18);
1305 30
            $wrap = explode('|', $this->solrConfiguration->getSearchResultsHighlightingWrap());
1306
1307 30
            if ($useFastVectorHighlighter) {
1308 28
                $this->queryParameters['hl.useFastVectorHighlighter'] = 'true';
1309 28
                $this->queryParameters['hl.tag.pre'] = $wrap[0];
1310 28
                $this->queryParameters['hl.tag.post'] = $wrap[1];
1311
            }
1312
1313 30
            if (isset($wrap[0]) && isset($wrap[1])) {
1314 25
                $this->queryParameters['hl.simple.pre'] = $wrap[0];
1315 30
                $this->queryParameters['hl.simple.post'] = $wrap[1];
1316
            }
1317
        } else {
1318
            // remove all hl.* settings
1319 1
            foreach ($this->queryParameters as $key => $value) {
1320 1
                if (GeneralUtility::isFirstPartOfStr($key, 'hl')) {
1321 1
                    unset($this->queryParameters[$key]);
1322
                }
1323
            }
1324
        }
1325 30
    }
1326
1327
    // misc
1328
1329
    /**
1330
     * Enables or disables spellchecking for the query.
1331
     *
1332
     * @param bool $spellchecking Enables spellchecking when set to TRUE, deactivates spellchecking when set to FALSE, defaults to TRUE.
1333
     */
1334 22
    public function setSpellchecking($spellchecking = true)
1335
    {
1336 22
        if ($spellchecking) {
1337 22
            $this->queryParameters['spellcheck'] = 'true';
1338 22
            $this->queryParameters['spellcheck.collate'] = 'true';
1339 22
            $maxCollationTries = $this->solrConfiguration->getSearchSpellcheckingNumberOfSuggestionsToTry();
1340 22
            $this->addQueryParameter('spellcheck.maxCollationTries', $maxCollationTries);
1341
        } else {
1342 1
            unset($this->queryParameters['spellcheck']);
1343 1
            unset($this->queryParameters['spellcheck.collate']);
1344 1
            unset($this->queryParameters['spellcheck.maxCollationTries']);
1345
        }
1346 22
    }
1347
1348
    /**
1349
     * Adds any generic query parameter.
1350
     *
1351
     * @param string $parameterName Query parameter name
1352
     * @param string $parameterValue Parameter value
1353
     */
1354 30
    public function addQueryParameter($parameterName, $parameterValue)
1355
    {
1356 30
        $this->queryParameters[$parameterName] = $parameterValue;
1357 30
    }
1358
1359
    /**
1360
     * Sets the sort parameter.
1361
     *
1362
     * $sorting must include a field name (or the pseudo-field score),
1363
     * followed by a space,
1364
     * followed by a sort direction (asc or desc).
1365
     *
1366
     * Multiple fallback sortings can be separated by comma,
1367
     * ie: <field name> <direction>[,<field name> <direction>]...
1368
     *
1369
     * @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)
1370
     * @see http://wiki.apache.org/solr/CommonQueryParameters#sort
1371
     */
1372 2
    public function setSorting($sorting)
1373
    {
1374 2
        if ($sorting) {
1375 2
            if (!is_string($sorting)) {
1376
                throw new \InvalidArgumentException("Sorting needs to be a string!");
1377
            }
1378 2
            $sortParameter = $this->removeRelevanceSortField($sorting);
1379 2
            $this->queryParameters['sort'] = $sortParameter;
1380
        } else {
1381 1
            unset($this->queryParameters['sort']);
1382
        }
1383 2
    }
1384
1385
    /**
1386
     * Removes the relevance sort field if present in the sorting field definition.
1387
     *
1388
     * @param string $sorting
1389
     * @return string
1390
     */
1391 2
    protected function removeRelevanceSortField($sorting)
1392
    {
1393 2
        $sortParameter = $sorting;
1394 2
        list($sortField) = explode(' ', $sorting);
1395 2
        if ($sortField == 'relevance') {
1396 1
            $sortParameter = '';
1397 1
            return $sortParameter;
1398
        }
1399
1400 2
        return $sortParameter;
1401
    }
1402
1403
    /**
1404
     * Enables or disables the debug parameter for the query.
1405
     *
1406
     * @param bool $debugMode Enables debugging when set to TRUE, deactivates debugging when set to FALSE, defaults to TRUE.
1407
     */
1408 20
    public function setDebugMode($debugMode = true)
1409
    {
1410 20
        if ($debugMode) {
1411 20
            $this->queryParameters['debugQuery'] = 'true';
1412 20
            $this->queryParameters['echoParams'] = 'all';
1413
        } else {
1414
            unset($this->queryParameters['debugQuery']);
1415
            unset($this->queryParameters['echoParams']);
1416
        }
1417 20
    }
1418
1419
    /**
1420
     * Returns the link target page id.
1421
     *
1422
     * @return int
1423
     */
1424 2
    public function getLinkTargetPageId()
1425
    {
1426 2
        return $this->linkTargetPageId;
1427
    }
1428
1429
    /**
1430
     * Activates the collapsing on the configured field, if collapsing was enabled.
1431
     *
1432
     * @return bool
1433
     */
1434 126
    protected function initializeCollapsingFromConfiguration()
1435
    {
1436
        // check collapsing
1437 126
        if ($this->solrConfiguration->getSearchVariants()) {
1438 2
            $collapseField = $this->solrConfiguration->getSearchVariantsField();
1439 2
            $this->setVariantField($collapseField);
1440 2
            $this->setCollapsing(true);
1441
1442 2
            return true;
1443
        }
1444
1445 124
        return false;
1446
    }
1447
}
1448