Passed
Push — master ( 220d88...d77450 )
by
unknown
12:58
created

SearchController::writeSearchStat()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 17
rs 9.9332
cc 4
nc 3
nop 1
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\IndexedSearch\Controller;
17
18
use Psr\Http\Message\ResponseInterface;
19
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
20
use TYPO3\CMS\Core\Context\Context;
21
use TYPO3\CMS\Core\Database\Connection;
22
use TYPO3\CMS\Core\Database\ConnectionPool;
23
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
24
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
25
use TYPO3\CMS\Core\Exception\Page\RootLineException;
26
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
27
use TYPO3\CMS\Core\Html\HtmlParser;
28
use TYPO3\CMS\Core\Site\SiteFinder;
29
use TYPO3\CMS\Core\Type\File\ImageInfo;
30
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
31
use TYPO3\CMS\Core\Utility\GeneralUtility;
32
use TYPO3\CMS\Core\Utility\MathUtility;
33
use TYPO3\CMS\Core\Utility\PathUtility;
34
use TYPO3\CMS\Core\Utility\RootlineUtility;
35
use TYPO3\CMS\Extbase\Annotation as Extbase;
36
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
37
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
38
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
39
use TYPO3\CMS\IndexedSearch\Domain\Repository\IndexSearchRepository;
40
use TYPO3\CMS\IndexedSearch\Lexer;
41
use TYPO3\CMS\IndexedSearch\Utility\IndexedSearchUtility;
42
43
/**
44
 * Index search frontend
45
 *
46
 * Creates a search form for indexed search. Indexing must be enabled
47
 * for this to make sense.
48
 * @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
49
 */
50
class SearchController extends ActionController
51
{
52
    /**
53
     * previously known as $this->piVars['sword']
54
     *
55
     * @var string
56
     */
57
    protected $sword = '';
58
59
    /**
60
     * @var array
61
     */
62
    protected $searchWords = [];
63
64
    /**
65
     * @var array
66
     */
67
    protected $searchData;
68
69
    /**
70
     * This is the id of the site root.
71
     * This value may be a comma separated list of integer (prepared for this)
72
     * Root-page PIDs to search in (rl0 field where clause, see initialize() function)
73
     *
74
     * If this value is set to less than zero (eg. -1) searching will happen
75
     * in ALL of the page tree with no regard to branches at all.
76
     * @var int|string
77
     */
78
    protected $searchRootPageIdList = 0;
79
80
    /**
81
     * @var int
82
     */
83
    protected $defaultResultNumber = 10;
84
85
    /**
86
     * @var int[]
87
     */
88
    protected $availableResultsNumbers = [];
89
90
    /**
91
     * Search repository
92
     *
93
     * @var \TYPO3\CMS\IndexedSearch\Domain\Repository\IndexSearchRepository
94
     */
95
    protected $searchRepository;
96
97
    /**
98
     * Lexer object
99
     *
100
     * @var \TYPO3\CMS\IndexedSearch\Lexer
101
     */
102
    protected $lexerObj;
103
104
    /**
105
     * External parser objects
106
     * @var array
107
     */
108
    protected $externalParsers = [];
109
110
    /**
111
     * Will hold the first row in result - used to calculate relative hit-ratings.
112
     *
113
     * @var array
114
     */
115
    protected $firstRow = [];
116
117
    /**
118
     * sys_domain records
119
     *
120
     * @var array
121
     */
122
    protected $domainRecords = [];
123
124
    /**
125
     * Required fe_groups memberships for display of a result.
126
     *
127
     * @var array
128
     */
129
    protected $requiredFrontendUsergroups = [];
130
131
    /**
132
     * Page tree sections for search result.
133
     *
134
     * @var array
135
     */
136
    protected $resultSections = [];
137
138
    /**
139
     * Caching of page path
140
     *
141
     * @var array
142
     */
143
    protected $pathCache = [];
144
145
    /**
146
     * Storage of icons
147
     *
148
     * @var array
149
     */
150
    protected $iconFileNameCache = [];
151
152
    /**
153
     * Indexer configuration, coming from TYPO3's system configuration for EXT:indexed_search
154
     *
155
     * @var array
156
     */
157
    protected $indexerConfig = [];
158
159
    /**
160
     * Flag whether metaphone search should be enabled
161
     *
162
     * @var bool
163
     */
164
    protected $enableMetaphoneSearch = false;
165
166
    /**
167
     * @var \TYPO3\CMS\Core\TypoScript\TypoScriptService
168
     */
169
    protected $typoScriptService;
170
171
    /**
172
     * @param \TYPO3\CMS\Core\TypoScript\TypoScriptService $typoScriptService
173
     */
174
    public function injectTypoScriptService(TypoScriptService $typoScriptService)
175
    {
176
        $this->typoScriptService = $typoScriptService;
177
    }
178
179
    /**
180
     * sets up all necessary object for searching
181
     *
182
     * @param array $searchData The incoming search parameters
183
     * @return array Search parameters
184
     */
185
    public function initialize($searchData = [])
186
    {
187
        if (!is_array($searchData)) {
0 ignored issues
show
introduced by
The condition is_array($searchData) is always true.
Loading history...
188
            $searchData = [];
189
        }
190
191
        // check if TypoScript is loaded
192
        if (!isset($this->settings['results'])) {
193
            $this->redirect('noTypoScript');
194
        }
195
196
        // Sets availableResultsNumbers - has to be called before request settings are read to avoid DoS attack
197
        $this->availableResultsNumbers = array_filter(GeneralUtility::intExplode(',', $this->settings['blind']['numberOfResults']));
198
199
        // Sets default result number if at least one availableResultsNumbers exists
200
        if (isset($this->availableResultsNumbers[0])) {
201
            $this->defaultResultNumber = $this->availableResultsNumbers[0];
202
        }
203
204
        $this->loadSettings();
205
206
        // setting default values
207
        if (is_array($this->settings['defaultOptions'])) {
208
            $searchData = array_merge($this->settings['defaultOptions'], $searchData);
209
        }
210
        // if "languageUid" was set to "current", take the current site language
211
        if (($searchData['languageUid'] ?? '') === 'current') {
212
            $searchData['languageUid'] = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('language', 'id', 0);
213
        }
214
215
        // Indexer configuration from Extension Manager interface:
216
        $this->indexerConfig = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('indexed_search');
217
        $this->enableMetaphoneSearch = (bool)$this->indexerConfig['enableMetaphoneSearch'];
218
        $this->initializeExternalParsers();
219
        // If "_sections" is set, this value overrides any existing value.
220
        if ($searchData['_sections']) {
221
            $searchData['sections'] = $searchData['_sections'];
222
        }
223
        // If "_sections" is set, this value overrides any existing value.
224
        if ($searchData['_freeIndexUid'] !== '' && $searchData['_freeIndexUid'] !== '_') {
225
            $searchData['freeIndexUid'] = $searchData['_freeIndexUid'];
226
        }
227
        $searchData['numberOfResults'] = $this->getNumberOfResults($searchData['numberOfResults']);
228
        // This gets the search-words into the $searchWordArray
229
        $this->setSword($searchData['sword']);
230
        // Add previous search words to current
231
        if ($searchData['sword_prev_include'] && $searchData['sword_prev']) {
232
            $this->setSword(trim($searchData['sword_prev']) . ' ' . $this->getSword());
233
        }
234
        $this->searchWords = $this->getSearchWords($searchData['defaultOperand']);
235
        // This is the id of the site root.
236
        // This value may be a commalist of integer (prepared for this)
237
        $this->searchRootPageIdList = (int)$GLOBALS['TSFE']->config['rootLine'][0]['uid'];
238
        // Setting the list of root PIDs for the search. Notice, these page IDs MUST
239
        // have a TypoScript template with root flag on them! Basically this list is used
240
        // to select on the "rl0" field and page ids are registered as "rl0" only if
241
        // a TypoScript template record with root flag is there.
242
        // This happens AFTER the use of $this->searchRootPageIdList above because
243
        // the above will then fetch the menu for the CURRENT site - regardless
244
        // of this kind of searching here. Thus a general search will lookup in
245
        // the WHOLE database while a specific section search will take the current sections.
246
        if ($this->settings['rootPidList']) {
247
            $this->searchRootPageIdList = implode(',', GeneralUtility::intExplode(',', $this->settings['rootPidList']));
248
        }
249
        $this->searchRepository = GeneralUtility::makeInstance(IndexSearchRepository::class);
250
        $this->searchRepository->initialize($this->settings, $searchData, $this->externalParsers, $this->searchRootPageIdList);
251
        $this->searchData = $searchData;
252
        // Calling hook for modification of initialized content
253
        if ($hookObj = $this->hookRequest('initialize_postProc')) {
254
            $hookObj->initialize_postProc();
255
        }
256
        return $searchData;
257
    }
258
259
    /**
260
     * Performs the search, the display and writing stats
261
     *
262
     * @param array $search the search parameters, an associative array
263
     * @Extbase\IgnoreValidation("search")
264
     */
265
    public function searchAction($search = []): ResponseInterface
266
    {
267
        $searchData = $this->initialize($search);
268
        // Find free index uid:
269
        $freeIndexUid = $searchData['freeIndexUid'];
270
        if ($freeIndexUid == -2) {
271
            $freeIndexUid = $this->settings['defaultFreeIndexUidList'];
272
        } elseif (!isset($searchData['freeIndexUid'])) {
273
            // index configuration is disabled
274
            $freeIndexUid = -1;
275
        }
276
277
        if (!empty($searchData['extendedSearch'])) {
278
            $this->view->assignMultiple($this->processExtendedSearchParameters());
279
        }
280
281
        $indexCfgs = GeneralUtility::intExplode(',', $freeIndexUid);
282
        $resultsets = [];
283
        foreach ($indexCfgs as $freeIndexUid) {
284
            // Get result rows
285
            if ($hookObj = $this->hookRequest('getResultRows')) {
286
                $resultData = $hookObj->getResultRows($this->searchWords, $freeIndexUid);
287
            } else {
288
                $resultData = $this->searchRepository->doSearch($this->searchWords, $freeIndexUid);
289
            }
290
            // Display search results
291
            if ($hookObj = $this->hookRequest('getDisplayResults')) {
292
                $resultsets[$freeIndexUid] = $hookObj->getDisplayResults($this->searchWords, $resultData, $freeIndexUid);
293
            } else {
294
                $resultsets[$freeIndexUid] = $this->getDisplayResults($this->searchWords, $resultData, $freeIndexUid);
295
            }
296
            // Create header if we are searching more than one indexing configuration
297
            if (count($indexCfgs) > 1) {
298
                if ($freeIndexUid > 0) {
299
                    $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
300
                        ->getQueryBuilderForTable('index_config');
301
                    $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
302
                    $indexCfgRec = $queryBuilder
303
                        ->select('title')
304
                        ->from('index_config')
305
                        ->where(
306
                            $queryBuilder->expr()->eq(
307
                                'uid',
308
                                $queryBuilder->createNamedParameter($freeIndexUid, \PDO::PARAM_INT)
309
                            )
310
                        )
311
                        ->execute()
312
                        ->fetch();
313
                    $categoryTitle = LocalizationUtility::translate('indexingConfigurationHeader.' . $freeIndexUid, 'IndexedSearch');
314
                    $categoryTitle = $categoryTitle ?: $indexCfgRec['title'];
315
                } else {
316
                    $categoryTitle = LocalizationUtility::translate('indexingConfigurationHeader.' . $freeIndexUid, 'IndexedSearch');
317
                }
318
                $resultsets[$freeIndexUid]['categoryTitle'] = $categoryTitle;
319
            }
320
            // Write search statistics
321
            $this->writeSearchStat($this->searchWords ?: []);
322
        }
323
        $this->view->assign('resultsets', $resultsets);
324
        $this->view->assign('searchParams', $searchData);
325
        $this->view->assign('searchWords', $this->searchWords);
326
327
        return $this->htmlResponse($this->view->render());
328
    }
329
330
    /****************************************
331
     * functions to make the result rows and result sets
332
     * ready for the output
333
     ***************************************/
334
    /**
335
     * Compiles the HTML display of the incoming array of result rows.
336
     *
337
     * @param array $searchWords Search words array (for display of text describing what was searched for)
338
     * @param array $resultData Array with result rows, count, first row.
339
     * @param int $freeIndexUid Pointing to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
340
     * @return array
341
     */
342
    protected function getDisplayResults($searchWords, $resultData, $freeIndexUid = -1)
343
    {
344
        $result = [
345
            'count' => $resultData['count'],
346
            'searchWords' => $searchWords
347
        ];
348
        // Perform display of result rows array
349
        if ($resultData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $resultData of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
350
            // Set first selected row (for calculation of ranking later)
351
            $this->firstRow = $resultData['firstRow'];
352
            // Result display here
353
            $result['rows'] = $this->compileResultRows($resultData['resultRows'], $freeIndexUid);
354
            $result['affectedSections'] = $this->resultSections;
355
            // Browsing box
356
            if ($resultData['count']) {
357
                // could we get this in the view?
358
                if ($this->searchData['group'] === 'sections' && $freeIndexUid <= 0) {
359
                    $resultSectionsCount = count($this->resultSections);
360
                    $result['sectionText'] = sprintf(LocalizationUtility::translate('result.' . ($resultSectionsCount > 1 ? 'inNsections' : 'inNsection'), 'IndexedSearch') ?? '', $resultSectionsCount);
361
                }
362
            }
363
        }
364
        // Print a message telling which words in which sections we searched for
365
        if (strpos($this->searchData['sections'], 'rl') === 0) {
366
            $result['searchedInSectionInfo'] = (LocalizationUtility::translate('result.inSection', 'IndexedSearch') ?? '') . ' "' . $this->getPathFromPageId((int)substr($this->searchData['sections'], 4)) . '"';
367
        }
368
369
        if ($hookObj = $this->hookRequest('getDisplayResults_postProc')) {
370
            $result = $hookObj->getDisplayResults_postProc($result);
371
        }
372
373
        return $result;
374
    }
375
376
    /**
377
     * Takes the array with resultrows as input and returns the result-HTML-code
378
     * Takes the "group" var into account: Makes a "section" or "flat" display.
379
     *
380
     * @param array $resultRows Result rows
381
     * @param int $freeIndexUid Pointing to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
382
     * @return array the result rows with additional information
383
     */
384
    protected function compileResultRows($resultRows, $freeIndexUid = -1)
385
    {
386
        $finalResultRows = [];
387
        // Transfer result rows to new variable,
388
        // performing some mapping of sub-results etc.
389
        $newResultRows = [];
390
        foreach ($resultRows as $row) {
391
            $id = md5($row['phash_grouping']);
392
            if (is_array($newResultRows[$id])) {
393
                // swapping:
394
                if (!$newResultRows[$id]['show_resume'] && $row['show_resume']) {
395
                    // Remove old
396
                    $subrows = $newResultRows[$id]['_sub'];
397
                    unset($newResultRows[$id]['_sub']);
398
                    $subrows[] = $newResultRows[$id];
399
                    // Insert new:
400
                    $newResultRows[$id] = $row;
401
                    $newResultRows[$id]['_sub'] = $subrows;
402
                } else {
403
                    $newResultRows[$id]['_sub'][] = $row;
404
                }
405
            } else {
406
                $newResultRows[$id] = $row;
407
            }
408
        }
409
        $resultRows = $newResultRows;
410
        $this->resultSections = [];
411
        if ($freeIndexUid <= 0 && $this->searchData['group'] === 'sections') {
412
            $rl2flag = strpos($this->searchData['sections'], 'rl') === 0;
413
            $sections = [];
414
            foreach ($resultRows as $row) {
415
                $id = $row['rl0'] . '-' . $row['rl1'] . ($rl2flag ? '-' . $row['rl2'] : '');
416
                $sections[$id][] = $row;
417
            }
418
            $this->resultSections = [];
419
            foreach ($sections as $id => $resultRows) {
0 ignored issues
show
introduced by
$resultRows is overwriting one of the parameters of this function.
Loading history...
420
                $rlParts = explode('-', $id);
421
                if ($rlParts[2]) {
422
                    $theId = $rlParts[2];
423
                    $theRLid = 'rl2_' . $rlParts[2];
424
                } elseif ($rlParts[1]) {
425
                    $theId = $rlParts[1];
426
                    $theRLid = 'rl1_' . $rlParts[1];
427
                } else {
428
                    $theId = $rlParts[0];
429
                    $theRLid = '0';
430
                }
431
                $sectionName = $this->getPathFromPageId((int)$theId);
432
                $sectionName = ltrim($sectionName, '/');
433
                if (!trim($sectionName)) {
434
                    $sectionTitleLinked = LocalizationUtility::translate('result.unnamedSection', 'IndexedSearch') . ':';
435
                } else {
436
                    $onclick = 'document.forms[\'tx_indexedsearch\'][\'tx_indexedsearch_pi2[search][_sections]\'].value=' . GeneralUtility::quoteJSvalue($theRLid) . ';document.forms[\'tx_indexedsearch\'].submit();return false;';
437
                    $sectionTitleLinked = '<a href="#" onclick="' . htmlspecialchars($onclick) . '">' . $sectionName . ':</a>';
438
                }
439
                $resultRowsCount = count($resultRows);
440
                $this->resultSections[$id] = [$sectionName, $resultRowsCount];
441
                // Add section header
442
                $finalResultRows[] = [
443
                    'isSectionHeader' => true,
444
                    'numResultRows' => $resultRowsCount,
445
                    'sectionId' => $id,
446
                    'sectionTitle' => $sectionTitleLinked
447
                ];
448
                // Render result rows
449
                foreach ($resultRows as $row) {
450
                    $finalResultRows[] = $this->compileSingleResultRow($row);
451
                }
452
            }
453
        } else {
454
            // flat mode or no sections at all
455
            foreach ($resultRows as $row) {
456
                $finalResultRows[] = $this->compileSingleResultRow($row);
457
            }
458
        }
459
        return $finalResultRows;
460
    }
461
462
    /**
463
     * This prints a single result row, including a recursive call for subrows.
464
     *
465
     * @param array $row Search result row
466
     * @param int $headerOnly 1=Display only header (for sub-rows!), 2=nothing at all
467
     * @return array the result row with additional information
468
     */
469
    protected function compileSingleResultRow($row, $headerOnly = 0)
470
    {
471
        $specRowConf = $this->getSpecialConfigurationForResultRow($row);
472
        $resultData = $row;
473
        $resultData['headerOnly'] = $headerOnly;
474
        $resultData['CSSsuffix'] = $specRowConf['CSSsuffix'] ? '-' . $specRowConf['CSSsuffix'] : '';
475
        if ($this->multiplePagesType($row['item_type'])) {
476
            $dat = json_decode($row['static_page_arguments'], true);
477
            $pp = explode('-', $dat['key']);
478
            if ($pp[0] != $pp[1]) {
479
                $resultData['titleaddition'] = ', ' . LocalizationUtility::translate('result.page', 'IndexedSearch') . ' ' . $dat['key'];
480
            } else {
481
                $resultData['titleaddition'] = ', ' . LocalizationUtility::translate('result.pages', 'IndexedSearch') . ' ' . $pp[0];
482
            }
483
        }
484
        $title = $resultData['item_title'] . $resultData['titleaddition'];
485
        $title = GeneralUtility::fixed_lgd_cs($title, $this->settings['results.']['titleCropAfter'], $this->settings['results.']['titleCropSignifier']);
486
        // If external media, link to the media-file instead.
487
        if ($row['item_type']) {
488
            if ($row['show_resume']) {
489
                // Can link directly.
490
                $targetAttribute = '';
491
                if ($GLOBALS['TSFE']->config['config']['fileTarget']) {
492
                    $targetAttribute = ' target="' . htmlspecialchars($GLOBALS['TSFE']->config['config']['fileTarget']) . '"';
493
                }
494
                $title = '<a href="' . htmlspecialchars($row['data_filename']) . '"' . $targetAttribute . '>' . htmlspecialchars($title) . '</a>';
495
            } else {
496
                // Suspicious, so linking to page instead...
497
                $copiedRow = $row;
498
                unset($copiedRow['static_page_arguments']);
499
                $title = $this->linkPageATagWrap(
500
                    $title,
501
                    $this->linkPage($row['page_id'], $copiedRow)
502
                );
503
            }
504
        } else {
505
            // Else the page:
506
            // Prepare search words for markup in content:
507
            $markUpSwParams = [];
508
            if ($this->settings['forwardSearchWordsInResultLink']['_typoScriptNodeValue']) {
509
                if ($this->settings['forwardSearchWordsInResultLink']['no_cache']) {
510
                    $markUpSwParams = ['no_cache' => 1];
511
                }
512
                foreach ($this->searchWords as $d) {
513
                    $markUpSwParams['sword_list'][] = $d['sword'];
514
                }
515
            }
516
            $title = $this->linkPageATagWrap(
517
                $title,
518
                $this->linkPage($row['data_page_id'], $row, $markUpSwParams)
519
            );
520
        }
521
        $resultData['title'] = $title;
522
        $resultData['icon'] = $this->makeItemTypeIcon($row['item_type'], '', $specRowConf);
523
        $resultData['rating'] = $this->makeRating($row);
524
        $resultData['description'] = $this->makeDescription(
525
            $row,
526
            (bool)!($this->searchData['extResume'] && !$headerOnly),
527
            $this->settings['results.']['summaryCropAfter']
528
        );
529
        $resultData['language'] = $this->makeLanguageIndication($row);
530
        $resultData['size'] = GeneralUtility::formatSize($row['item_size']);
531
        $resultData['created'] = $row['item_crdate'];
532
        $resultData['modified'] = $row['item_mtime'];
533
        $pI = parse_url($row['data_filename']);
534
        if ($pI['scheme']) {
535
            $targetAttribute = '';
536
            if ($GLOBALS['TSFE']->config['config']['fileTarget']) {
537
                $targetAttribute = ' target="' . htmlspecialchars($GLOBALS['TSFE']->config['config']['fileTarget']) . '"';
538
            }
539
            $resultData['pathTitle'] = $row['data_filename'];
540
            $resultData['pathUri'] = $row['data_filename'];
541
            $resultData['path'] = '<a href="' . htmlspecialchars($row['data_filename']) . '"' . $targetAttribute . '>' . htmlspecialchars($row['data_filename']) . '</a>';
542
        } else {
543
            $pathId = $row['data_page_id'] ?: $row['page_id'];
544
            $pathMP = $row['data_page_id'] ? $row['data_page_mp'] : '';
545
            $pathStr = $this->getPathFromPageId($pathId, $pathMP);
546
            $pathLinkData = $this->linkPage(
547
                $pathId,
548
                [
549
                    'data_page_type' => $row['data_page_type'],
550
                    'data_page_mp' => $pathMP,
551
                    'sys_language_uid' => $row['sys_language_uid'],
552
                    'static_page_arguments' => $row['static_page_arguments']
553
                ]
554
            );
555
556
            $resultData['pathTitle'] = $pathStr;
557
            $resultData['pathUri'] = $pathLinkData['uri'];
558
            $resultData['path'] = $this->linkPageATagWrap($pathStr, $pathLinkData);
559
560
            // check if the access is restricted
561
            if (is_array($this->requiredFrontendUsergroups[$pathId]) && !empty($this->requiredFrontendUsergroups[$pathId])) {
562
                $lockedIcon = GeneralUtility::getFileAbsFileName('EXT:indexed_search/Resources/Public/Icons/FileTypes/locked.gif');
563
                $lockedIcon = PathUtility::getAbsoluteWebPath($lockedIcon);
564
                $resultData['access'] = '<img src="' . htmlspecialchars($lockedIcon) . '"'
565
                    . ' width="12" height="15" vspace="5" title="'
566
                    . sprintf(LocalizationUtility::translate('result.memberGroups', 'IndexedSearch') ?? '', implode(',', array_unique($this->requiredFrontendUsergroups[$pathId])))
567
                    . '" alt="" />';
568
            }
569
        }
570
        // If there are subrows (eg. subpages in a PDF-file or if a duplicate page
571
        // is selected due to user-login (phash_grouping))
572
        if (is_array($row['_sub'])) {
573
            $resultData['subresults'] = [];
574
            if ($this->multiplePagesType($row['item_type'])) {
575
                $resultData['subresults']['header'] = LocalizationUtility::translate('result.otherMatching', 'IndexedSearch');
576
                foreach ($row['_sub'] as $subRow) {
577
                    $resultData['subresults']['items'][] = $this->compileSingleResultRow($subRow, 1);
578
                }
579
            } else {
580
                $resultData['subresults']['header'] = LocalizationUtility::translate('result.otherMatching', 'IndexedSearch');
581
                $resultData['subresults']['info'] = LocalizationUtility::translate('result.otherPageAsWell', 'IndexedSearch');
582
            }
583
        }
584
        return $resultData;
585
    }
586
587
    /**
588
     * Returns configuration from TypoScript for result row based
589
     * on ID / location in page tree!
590
     *
591
     * @param array $row Result row
592
     * @return array Configuration array
593
     */
594
    protected function getSpecialConfigurationForResultRow($row)
595
    {
596
        $pathId = $row['data_page_id'] ?: $row['page_id'];
597
        $pathMP = $row['data_page_id'] ? $row['data_page_mp'] : '';
598
        $specConf = $this->settings['specialConfiguration']['0'];
599
        try {
600
            $rl = GeneralUtility::makeInstance(RootlineUtility::class, $pathId, $pathMP)->get();
601
            foreach ($rl as $dat) {
602
                if (is_array($this->settings['specialConfiguration'][$dat['uid']])) {
603
                    $specConf = $this->settings['specialConfiguration'][$dat['uid']];
604
                    $specConf['_pid'] = $dat['uid'];
605
                    break;
606
                }
607
            }
608
        } catch (RootLineException $e) {
609
            // do nothing
610
        }
611
        return $specConf;
612
    }
613
614
    /**
615
     * Return the rating-HTML code for the result row. This makes use of the $this->firstRow
616
     *
617
     * @param array $row Result row array
618
     * @return string String showing ranking value
619
     * @todo can this be a ViewHelper?
620
     */
621
    protected function makeRating($row)
622
    {
623
        $default = ' ';
624
        switch ((string)$this->searchData['sortOrder']) {
625
            case 'rank_count':
626
                return $row['order_val'] . ' ' . LocalizationUtility::translate('result.ratingMatches', 'IndexedSearch');
627
            case 'rank_first':
628
                return ceil(MathUtility::forceIntegerInRange(255 - $row['order_val'], 1, 255) / 255 * 100) . '%';
629
            case 'rank_flag':
630
                if ($this->firstRow['order_val2']) {
631
                    // (3 MSB bit, 224 is highest value of order_val1 currently)
632
                    $base = $row['order_val1'] * 256;
633
                    // 15-3 MSB = 12
634
                    $freqNumber = $row['order_val2'] / $this->firstRow['order_val2'] * 2 ** 12;
635
                    $total = MathUtility::forceIntegerInRange($base + $freqNumber, 0, 32767);
636
                    return ceil(log($total) / log(32767) * 100) . '%';
637
                }
638
                return $default;
639
            case 'rank_freq':
640
                $max = 10000;
641
                $total = MathUtility::forceIntegerInRange($row['order_val'], 0, $max);
642
                return ceil(log($total) / log($max) * 100) . '%';
643
            case 'crdate':
644
                return $GLOBALS['TSFE']->cObj->calcAge($GLOBALS['EXEC_TIME'] - $row['item_crdate'], 0);
645
            case 'mtime':
646
                return $GLOBALS['TSFE']->cObj->calcAge($GLOBALS['EXEC_TIME'] - $row['item_mtime'], 0);
647
            default:
648
                return $default;
649
        }
650
    }
651
652
    /**
653
     * Returns the HTML code for language indication.
654
     *
655
     * @param array $row Result row
656
     * @return string HTML code for result row.
657
     */
658
    protected function makeLanguageIndication($row)
659
    {
660
        $output = '&nbsp;';
661
        // If search result is a TYPO3 page:
662
        if ((string)$row['item_type'] === '0') {
663
            // If TypoScript is used to render the flag:
664
            if (is_array($this->settings['flagRendering'])) {
665
                /** @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $cObj */
666
                $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
667
                $cObj->setCurrentVal($row['sys_language_uid']);
668
                $typoScriptArray = $this->typoScriptService->convertPlainArrayToTypoScriptArray($this->settings['flagRendering']);
669
                $output = $cObj->cObjGetSingle($this->settings['flagRendering']['_typoScriptNodeValue'], $typoScriptArray);
670
            }
671
        }
672
        return $output;
673
    }
674
675
    /**
676
     * Return icon for file extension
677
     *
678
     * @param string $imageType File extension / item type
679
     * @param string $alt Title attribute value in icon.
680
     * @param array $specRowConf TypoScript configuration specifically for search result.
681
     * @return string HTML <img> tag for icon
682
     */
683
    public function makeItemTypeIcon($imageType, $alt, $specRowConf)
684
    {
685
        // Build compound key if item type is 0, iconRendering is not used
686
        // and specialConfiguration.[pid].pageIcon was set in TS
687
        if ($imageType === '0' && $specRowConf['_pid'] && is_array($specRowConf['pageIcon']) && !is_array($this->settings['iconRendering'])) {
688
            $imageType .= ':' . $specRowConf['_pid'];
689
        }
690
        if (!isset($this->iconFileNameCache[$imageType])) {
691
            $this->iconFileNameCache[$imageType] = '';
692
            // If TypoScript is used to render the icon:
693
            if (is_array($this->settings['iconRendering'])) {
694
                /** @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $cObj */
695
                $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
696
                $cObj->setCurrentVal($imageType);
697
                $typoScriptArray = $this->typoScriptService->convertPlainArrayToTypoScriptArray($this->settings['iconRendering']);
698
                $this->iconFileNameCache[$imageType] = $cObj->cObjGetSingle($this->settings['iconRendering']['_typoScriptNodeValue'], $typoScriptArray);
699
            } else {
700
                // Default creation / finding of icon:
701
                $icon = '';
702
                if ($imageType === '0' || strpos($imageType, '0:') === 0) {
703
                    if (is_array($specRowConf['pageIcon'])) {
704
                        $this->iconFileNameCache[$imageType] = $GLOBALS['TSFE']->cObj->cObjGetSingle('IMAGE', $specRowConf['pageIcon']);
705
                    } else {
706
                        $icon = 'EXT:indexed_search/Resources/Public/Icons/FileTypes/pages.gif';
707
                    }
708
                } elseif ($this->externalParsers[$imageType]) {
709
                    $icon = $this->externalParsers[$imageType]->getIcon($imageType);
710
                }
711
                if ($icon) {
712
                    $fullPath = GeneralUtility::getFileAbsFileName($icon);
713
                    if ($fullPath) {
714
                        $imageInfo = GeneralUtility::makeInstance(ImageInfo::class, $fullPath);
715
                        $iconPath = PathUtility::stripPathSitePrefix($fullPath);
716
                        $this->iconFileNameCache[$imageType] = $imageInfo->getWidth()
717
                            ? '<img src="' . $iconPath
718
                              . '" width="' . $imageInfo->getWidth()
719
                              . '" height="' . $imageInfo->getHeight()
720
                              . '" title="' . htmlspecialchars($alt) . '" alt="" />'
721
                            : '';
722
                    }
723
                }
724
            }
725
        }
726
        return $this->iconFileNameCache[$imageType];
727
    }
728
729
    /**
730
     * Returns the resume for the search-result.
731
     *
732
     * @param array $row Search result row
733
     * @param bool $noMarkup If noMarkup is FALSE, then the index_fulltext table is used to select the content of the page, split it with regex to display the search words in the text.
734
     * @param int $length String length
735
     * @return string HTML string
736
     * @todo overwork this
737
     */
738
    protected function makeDescription($row, $noMarkup = false, $length = 180)
739
    {
740
        $markedSW = '';
741
        $outputStr = '';
742
        if ($row['show_resume']) {
743
            if (!$noMarkup) {
744
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('index_fulltext');
745
                $ftdrow = $queryBuilder
746
                    ->select('*')
747
                    ->from('index_fulltext')
748
                    ->where(
749
                        $queryBuilder->expr()->eq(
750
                            'phash',
751
                            $queryBuilder->createNamedParameter($row['phash'], \PDO::PARAM_INT)
752
                        )
753
                    )
754
                    ->execute()
755
                    ->fetch();
756
                if ($ftdrow !== false) {
757
                    // Cut HTTP references after some length
758
                    $content = preg_replace('/(http:\\/\\/[^ ]{' . $this->settings['results.']['hrefInSummaryCropAfter'] . '})([^ ]+)/i', '$1...', $ftdrow['fulltextdata']);
759
                    $markedSW = $this->markupSWpartsOfString($content);
760
                }
761
            }
762
            if (!trim($markedSW)) {
763
                $outputStr = GeneralUtility::fixed_lgd_cs($row['item_description'], $length, $this->settings['results.']['summaryCropSignifier']);
764
                $outputStr = htmlspecialchars($outputStr);
765
            }
766
            $output = $outputStr ?: $markedSW;
767
        } else {
768
            $output = '<span class="noResume">' . LocalizationUtility::translate('result.noResume', 'IndexedSearch') . '</span>';
769
        }
770
        return $output;
771
    }
772
773
    /**
774
     * Marks up the search words from $this->searchWords in the $str with a color.
775
     *
776
     * @param string $str Text in which to find and mark up search words. This text is assumed to be UTF-8 like the search words internally is.
777
     * @return string Processed content
778
     */
779
    protected function markupSWpartsOfString($str)
780
    {
781
        $htmlParser = GeneralUtility::makeInstance(HtmlParser::class);
782
        // Init:
783
        $str = str_replace('&nbsp;', ' ', $htmlParser->bidir_htmlspecialchars($str, -1));
784
        $str = preg_replace('/\\s\\s+/', ' ', $str);
785
        $swForReg = [];
786
        // Prepare search words for regex:
787
        foreach ($this->searchWords as $d) {
788
            $swForReg[] = preg_quote($d['sword'], '/');
789
        }
790
        $regExString = '(' . implode('|', $swForReg) . ')';
791
        // Split and combine:
792
        $parts = preg_split('/' . $regExString . '/i', ' ' . $str . ' ', 20000, PREG_SPLIT_DELIM_CAPTURE);
793
        $parts = $parts ?: [];
794
        // Constants:
795
        $summaryMax = $this->settings['results.']['markupSW_summaryMax'];
796
        $postPreLgd = (int)$this->settings['results.']['markupSW_postPreLgd'];
797
        $postPreLgd_offset = (int)$this->settings['results.']['markupSW_postPreLgd_offset'];
798
        $divider = $this->settings['results.']['markupSW_divider'];
799
        $occurrences = (count($parts) - 1) / 2;
800
        if ($occurrences) {
801
            $postPreLgd = MathUtility::forceIntegerInRange($summaryMax / $occurrences, $postPreLgd, $summaryMax / 2);
802
        }
803
        // Variable:
804
        $summaryLgd = 0;
805
        $output = [];
806
        // Shorten in-between strings:
807
        foreach ($parts as $k => $strP) {
808
            if ($k % 2 == 0) {
809
                // Find length of the summary part:
810
                $strLen = mb_strlen($parts[$k], 'utf-8');
811
                $output[$k] = $parts[$k];
812
                // Possibly shorten string:
813
                if (!$k) {
814
                    // First entry at all (only cropped on the frontside)
815
                    if ($strLen > $postPreLgd) {
816
                        $output[$k] = $divider . preg_replace('/^[^[:space:]]+[[:space:]]/', '', GeneralUtility::fixed_lgd_cs($parts[$k], -($postPreLgd - $postPreLgd_offset)));
817
                    }
818
                } elseif ($summaryLgd > $summaryMax || !isset($parts[$k + 1])) {
819
                    // In case summary length is exceed OR if there are no more entries at all:
820
                    if ($strLen > $postPreLgd) {
821
                        $output[$k] = preg_replace('/[[:space:]][^[:space:]]+$/', '', GeneralUtility::fixed_lgd_cs(
822
                            $parts[$k],
823
                            $postPreLgd - $postPreLgd_offset
824
                        )) . $divider;
825
                    }
826
                } else {
827
                    if ($strLen > $postPreLgd * 2) {
828
                        $output[$k] = preg_replace('/[[:space:]][^[:space:]]+$/', '', GeneralUtility::fixed_lgd_cs(
829
                            $parts[$k],
830
                            $postPreLgd - $postPreLgd_offset
831
                        )) . $divider . preg_replace('/^[^[:space:]]+[[:space:]]/', '', GeneralUtility::fixed_lgd_cs($parts[$k], -($postPreLgd - $postPreLgd_offset)));
832
                    }
833
                }
834
                $summaryLgd += mb_strlen($output[$k], 'utf-8');
835
                // Protect output:
836
                $output[$k] = htmlspecialchars($output[$k]);
837
                // If summary lgd is exceed, break the process:
838
                if ($summaryLgd > $summaryMax) {
839
                    break;
840
                }
841
            } else {
842
                $summaryLgd += mb_strlen($strP, 'utf-8');
843
                $output[$k] = '<strong class="tx-indexedsearch-redMarkup">' . htmlspecialchars($parts[$k]) . '</strong>';
844
            }
845
        }
846
        // Return result:
847
        return implode('', $output);
848
    }
849
850
    /**
851
     * Write statistics information to database for the search operation if there was at least one search word.
852
     *
853
     * @param array $searchWords Search Word array
854
     */
855
    protected function writeSearchStat(array $searchWords): void
856
    {
857
        if (empty($this->getSword()) && empty($searchWords)) {
858
            return;
859
        }
860
        $entries = [];
861
        foreach ($searchWords as $val) {
862
            $entries[] = [
863
                'word' => $val['sword'],
864
                // Time stamp
865
                'tstamp' => $GLOBALS['EXEC_TIME'],
866
                // search page id for indexed search stats
867
                'pageid' => $GLOBALS['TSFE']->id
868
            ];
869
        }
870
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('index_stat_word');
871
        $connection->bulkInsert('index_stat_word', $entries);
872
    }
873
874
    /**
875
     * Splits the search word input into an array where each word is represented by an array with key "sword"
876
     * holding the search word and key "oper" holding the SQL operator (eg. AND, OR)
877
     *
878
     * Only words with 2 or more characters are accepted
879
     * Max 200 chars total
880
     * Space is used to split words, "" can be used search for a whole string
881
     * AND, OR and NOT are prefix words, overruling the default operator
882
     * +/|/- equals AND, OR and NOT as operators.
883
     * All search words are converted to lowercase.
884
     *
885
     * $defOp is the default operator. 1=OR, 0=AND
886
     *
887
     * @param bool $defaultOperator If TRUE, the default operator will be OR, not AND
888
     * @return array Search words if any found
889
     */
890
    protected function getSearchWords($defaultOperator)
891
    {
892
        // Shorten search-word string to max 200 bytes - shortening the string here is only a run-away feature!
893
        $searchWords = mb_substr($this->getSword(), 0, 200);
894
        // Convert to UTF-8 + conv. entities (was also converted during indexing!)
895
        if ($GLOBALS['TSFE']->metaCharset && $GLOBALS['TSFE']->metaCharset !== 'utf-8') {
896
            $searchWords = mb_convert_encoding($searchWords, 'utf-8', $GLOBALS['TSFE']->metaCharset);
897
            $searchWords = html_entity_decode($searchWords);
898
        }
899
        $sWordArray = false;
900
        if ($hookObj = $this->hookRequest('getSearchWords')) {
901
            $sWordArray = $hookObj->getSearchWords_splitSWords($searchWords, $defaultOperator);
902
        } else {
903
            // sentence
904
            if ($this->searchData['searchType'] == 20) {
905
                $sWordArray = [
906
                    [
907
                        'sword' => trim($searchWords),
908
                        'oper' => 'AND'
909
                    ]
910
                ];
911
            } else {
912
                // case-sensitive. Defines the words, which will be
913
                // operators between words
914
                $operatorTranslateTable = [
915
                    ['+', 'AND'],
916
                    ['|', 'OR'],
917
                    ['-', 'AND NOT'],
918
                    // Add operators for various languages
919
                    // Converts the operators to lowercase
920
                    [mb_strtolower(LocalizationUtility::translate('localizedOperandAnd', 'IndexedSearch') ?? '', 'utf-8'), 'AND'],
921
                    [mb_strtolower(LocalizationUtility::translate('localizedOperandOr', 'IndexedSearch') ?? '', 'utf-8'), 'OR'],
922
                    [mb_strtolower(LocalizationUtility::translate('localizedOperandNot', 'IndexedSearch') ?? '', 'utf-8'), 'AND NOT']
923
                ];
924
                $swordArray = IndexedSearchUtility::getExplodedSearchString($searchWords, $defaultOperator == 1 ? 'OR' : 'AND', $operatorTranslateTable);
925
                if (is_array($swordArray)) {
0 ignored issues
show
introduced by
The condition is_array($swordArray) is always true.
Loading history...
926
                    $sWordArray = $this->procSearchWordsByLexer($swordArray);
927
                }
928
            }
929
        }
930
        return $sWordArray;
931
    }
932
933
    /**
934
     * Post-process the search word array so it will match the words that was indexed (including case-folding if any)
935
     * If any words are splitted into multiple words (eg. CJK will be!) the operator of the main word will remain.
936
     *
937
     * @param array $searchWords Search word array
938
     * @return array Search word array, processed through lexer
939
     */
940
    protected function procSearchWordsByLexer($searchWords)
941
    {
942
        $newSearchWords = [];
943
        // Init lexer (used to post-processing of search words)
944
        $lexerObjectClassName = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['lexer'] ?: Lexer::class;
945
        $this->lexerObj = GeneralUtility::makeInstance($lexerObjectClassName);
946
        // Traverse the search word array
947
        foreach ($searchWords as $wordDef) {
948
            // No space in word (otherwise it might be a sentence in quotes like "there is").
949
            if (strpos($wordDef['sword'], ' ') === false) {
950
                // Split the search word by lexer:
951
                $res = $this->lexerObj->split2Words($wordDef['sword']);
952
                // Traverse lexer result and add all words again:
953
                foreach ($res as $word) {
954
                    $newSearchWords[] = [
955
                        'sword' => $word,
956
                        'oper' => $wordDef['oper']
957
                    ];
958
                }
959
            } else {
960
                $newSearchWords[] = $wordDef;
961
            }
962
        }
963
        return $newSearchWords;
964
    }
965
966
    /**
967
     * Sort options about the search form
968
     *
969
     * @param array $search The search data / params
970
     * @Extbase\IgnoreValidation("search")
971
     */
972
    public function formAction($search = []): ResponseInterface
973
    {
974
        $searchData = $this->initialize($search);
975
        // Adding search field value
976
        $this->view->assign('sword', $this->getSword());
977
        // Extended search
978
        if (!empty($searchData['extendedSearch'])) {
979
            $this->view->assignMultiple($this->processExtendedSearchParameters());
980
        }
981
        $this->view->assign('searchParams', $searchData);
982
983
        return $this->htmlResponse($this->view->render());
984
    }
985
986
    /**
987
     * TypoScript was not loaded
988
     */
989
    public function noTypoScriptAction(): ResponseInterface
990
    {
991
        return $this->htmlResponse($this->view->render());
992
    }
993
994
    /****************************************
995
     * building together the available options for every dropdown
996
     ***************************************/
997
    /**
998
     * get the values for the "type" selector
999
     *
1000
     * @return array Associative array with options
1001
     */
1002
    protected function getAllAvailableSearchTypeOptions()
1003
    {
1004
        $allOptions = [];
1005
        $types = [0, 1, 2, 3, 10, 20];
1006
        $blindSettings = $this->settings['blind'];
1007
        if (!$blindSettings['searchType']) {
1008
            foreach ($types as $typeNum) {
1009
                $allOptions[$typeNum] = LocalizationUtility::translate('searchTypes.' . $typeNum, 'IndexedSearch');
1010
            }
1011
        }
1012
        // Remove this option if metaphone search is disabled)
1013
        if (!$this->enableMetaphoneSearch) {
1014
            unset($allOptions[10]);
1015
        }
1016
        // disable single entries by TypoScript
1017
        $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['searchType']);
1018
        return $allOptions;
1019
    }
1020
1021
    /**
1022
     * get the values for the "defaultOperand" selector
1023
     *
1024
     * @return array Associative array with options
1025
     */
1026
    protected function getAllAvailableOperandsOptions()
1027
    {
1028
        $allOptions = [];
1029
        $blindSettings = $this->settings['blind'];
1030
        if (!$blindSettings['defaultOperand']) {
1031
            $allOptions = [
1032
                0 => LocalizationUtility::translate('defaultOperands.0', 'IndexedSearch'),
1033
                1 => LocalizationUtility::translate('defaultOperands.1', 'IndexedSearch')
1034
            ];
1035
        }
1036
        // disable single entries by TypoScript
1037
        $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['defaultOperand']);
1038
        return $allOptions;
1039
    }
1040
1041
    /**
1042
     * get the values for the "media type" selector
1043
     *
1044
     * @return array Associative array with options
1045
     */
1046
    protected function getAllAvailableMediaTypesOptions()
1047
    {
1048
        $allOptions = [];
1049
        $mediaTypes = [-1, 0, -2];
1050
        $blindSettings = $this->settings['blind'];
1051
        if (!$blindSettings['mediaType']) {
1052
            foreach ($mediaTypes as $mediaType) {
1053
                $allOptions[$mediaType] = LocalizationUtility::translate('mediaTypes.' . $mediaType, 'IndexedSearch');
1054
            }
1055
            // Add media to search in:
1056
            $additionalMedia = trim($this->settings['mediaList']);
1057
            if ($additionalMedia !== '') {
1058
                $additionalMedia = GeneralUtility::trimExplode(',', $additionalMedia, true);
1059
            } else {
1060
                $additionalMedia = [];
1061
            }
1062
            foreach ($this->externalParsers as $extension => $obj) {
1063
                // Skip unwanted extensions
1064
                if (!empty($additionalMedia) && !in_array($extension, $additionalMedia)) {
1065
                    continue;
1066
                }
1067
                if ($name = $obj->searchTypeMediaTitle($extension)) {
1068
                    $translatedName = LocalizationUtility::translate('mediaTypes.' . $extension, 'IndexedSearch');
1069
                    $allOptions[$extension] = $translatedName ?: $name;
1070
                }
1071
            }
1072
        }
1073
        // disable single entries by TypoScript
1074
        $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['mediaType']);
1075
        return $allOptions;
1076
    }
1077
1078
    /**
1079
     * get the values for the "language" selector
1080
     *
1081
     * @return array Associative array with options
1082
     */
1083
    protected function getAllAvailableLanguageOptions()
1084
    {
1085
        $allOptions = [
1086
            '-1' => LocalizationUtility::translate('languageUids.-1', 'IndexedSearch')
1087
        ];
1088
        $blindSettings = $this->settings['blind'];
1089
        if (!$blindSettings['languageUid']) {
1090
            try {
1091
                $site = GeneralUtility::makeInstance(SiteFinder::class)
1092
                    ->getSiteByPageId($GLOBALS['TSFE']->id);
1093
1094
                $languages = $site->getLanguages();
1095
                foreach ($languages as $language) {
1096
                    $allOptions[$language->getLanguageId()] = $language->getNavigationTitle() ?? $language->getTitle();
1097
                }
1098
            } catch (SiteNotFoundException $e) {
1099
                // No Site found, no options
1100
                $allOptions = [];
1101
            }
1102
1103
            // disable single entries by TypoScript
1104
            $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['languageUid']);
1105
        } else {
1106
            $allOptions = [];
1107
        }
1108
        return $allOptions;
1109
    }
1110
1111
    /**
1112
     * get the values for the "section" selector
1113
     * Here values like "rl1_" and "rl2_" + a rootlevel 1/2 id can be added
1114
     * to perform searches in rootlevel 1+2 specifically. The id-values can even
1115
     * be commaseparated. Eg. "rl1_1,2" would search for stuff inside pages on
1116
     * menu-level 1 which has the uid's 1 and 2.
1117
     *
1118
     * @return array Associative array with options
1119
     */
1120
    protected function getAllAvailableSectionsOptions()
1121
    {
1122
        $allOptions = [];
1123
        $sections = [0, -1, -2, -3];
1124
        $blindSettings = $this->settings['blind'];
1125
        if (!$blindSettings['sections']) {
1126
            foreach ($sections as $section) {
1127
                $allOptions[$section] = LocalizationUtility::translate('sections.' . $section, 'IndexedSearch');
1128
            }
1129
        }
1130
        // Creating levels for section menu:
1131
        // This selects the first and secondary menus for the "sections" selector - so we can search in sections and sub sections.
1132
        if ($this->settings['displayLevel1Sections']) {
1133
            $firstLevelMenu = $this->getMenuOfPages((int)$this->searchRootPageIdList);
1134
            $labelLevel1 = LocalizationUtility::translate('sections.rootLevel1', 'IndexedSearch');
1135
            $labelLevel2 = LocalizationUtility::translate('sections.rootLevel2', 'IndexedSearch');
1136
            foreach ($firstLevelMenu as $firstLevelKey => $menuItem) {
1137
                if (!$menuItem['nav_hide']) {
1138
                    $allOptions['rl1_' . $menuItem['uid']] = trim($labelLevel1 . ' ' . $menuItem['title']);
1139
                    if ($this->settings['displayLevel2Sections']) {
1140
                        $secondLevelMenu = $this->getMenuOfPages($menuItem['uid']);
1141
                        foreach ($secondLevelMenu as $secondLevelKey => $menuItemLevel2) {
1142
                            if (!$menuItemLevel2['nav_hide']) {
1143
                                $allOptions['rl2_' . $menuItemLevel2['uid']] = trim($labelLevel2 . ' ' . $menuItemLevel2['title']);
1144
                            } else {
1145
                                unset($secondLevelMenu[$secondLevelKey]);
1146
                            }
1147
                        }
1148
                        $allOptions['rl2_' . implode(',', array_keys($secondLevelMenu))] = LocalizationUtility::translate('sections.rootLevel2All', 'IndexedSearch');
1149
                    }
1150
                } else {
1151
                    unset($firstLevelMenu[$firstLevelKey]);
1152
                }
1153
            }
1154
            $allOptions['rl1_' . implode(',', array_keys($firstLevelMenu))] = LocalizationUtility::translate('sections.rootLevel1All', 'IndexedSearch');
1155
        }
1156
        // disable single entries by TypoScript
1157
        $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['sections']);
1158
        return $allOptions;
1159
    }
1160
1161
    /**
1162
     * get the values for the "freeIndexUid" selector
1163
     *
1164
     * @return array Associative array with options
1165
     */
1166
    protected function getAllAvailableIndexConfigurationsOptions()
1167
    {
1168
        $allOptions = [
1169
            '-1' => LocalizationUtility::translate('indexingConfigurations.-1', 'IndexedSearch'),
1170
            '-2' => LocalizationUtility::translate('indexingConfigurations.-2', 'IndexedSearch'),
1171
            '0' => LocalizationUtility::translate('indexingConfigurations.0', 'IndexedSearch')
1172
        ];
1173
        $blindSettings = $this->settings['blind'];
1174
        if (!$blindSettings['indexingConfigurations']) {
1175
            // add an additional index configuration
1176
            if ($this->settings['defaultFreeIndexUidList']) {
1177
                $uidList = GeneralUtility::intExplode(',', $this->settings['defaultFreeIndexUidList']);
1178
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1179
                    ->getQueryBuilderForTable('index_config');
1180
                $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
1181
                $result = $queryBuilder
1182
                    ->select('uid', 'title')
1183
                    ->from('index_config')
1184
                    ->where(
1185
                        $queryBuilder->expr()->in(
1186
                            'uid',
1187
                            $queryBuilder->createNamedParameter($uidList, Connection::PARAM_INT_ARRAY)
1188
                        )
1189
                    )
1190
                    ->execute();
1191
1192
                while ($row = $result->fetch()) {
1193
                    $indexId = (int)$row['uid'];
1194
                    $title = LocalizationUtility::translate('indexingConfigurations.' . $indexId, 'IndexedSearch');
1195
                    $allOptions[$indexId] = $title ?: $row['title'];
1196
                }
1197
            }
1198
            // disable single entries by TypoScript
1199
            $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['indexingConfigurations']);
1200
        } else {
1201
            $allOptions = [];
1202
        }
1203
        return $allOptions;
1204
    }
1205
1206
    /**
1207
     * get the values for the "section" selector
1208
     * Here values like "rl1_" and "rl2_" + a rootlevel 1/2 id can be added
1209
     * to perform searches in rootlevel 1+2 specifically. The id-values can even
1210
     * be commaseparated. Eg. "rl1_1,2" would search for stuff inside pages on
1211
     * menu-level 1 which has the uid's 1 and 2.
1212
     *
1213
     * @return array Associative array with options
1214
     */
1215
    protected function getAllAvailableSortOrderOptions()
1216
    {
1217
        $allOptions = [];
1218
        $sortOrders = ['rank_flag', 'rank_freq', 'rank_first', 'rank_count', 'mtime', 'title', 'crdate'];
1219
        $blindSettings = $this->settings['blind'];
1220
        if (!$blindSettings['sortOrder']) {
1221
            foreach ($sortOrders as $order) {
1222
                $allOptions[$order] = LocalizationUtility::translate('sortOrders.' . $order, 'IndexedSearch');
1223
            }
1224
        }
1225
        // disable single entries by TypoScript
1226
        $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['sortOrder.']);
1227
        return $allOptions;
1228
    }
1229
1230
    /**
1231
     * get the values for the "group" selector
1232
     *
1233
     * @return array Associative array with options
1234
     */
1235
    protected function getAllAvailableGroupOptions()
1236
    {
1237
        $allOptions = [];
1238
        $blindSettings = $this->settings['blind'];
1239
        if (!$blindSettings['groupBy']) {
1240
            $allOptions = [
1241
                'sections' => LocalizationUtility::translate('groupBy.sections', 'IndexedSearch'),
1242
                'flat' => LocalizationUtility::translate('groupBy.flat', 'IndexedSearch')
1243
            ];
1244
        }
1245
        // disable single entries by TypoScript
1246
        $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['groupBy.']);
1247
        return $allOptions;
1248
    }
1249
1250
    /**
1251
     * get the values for the "sortDescending" selector
1252
     *
1253
     * @return array Associative array with options
1254
     */
1255
    protected function getAllAvailableSortDescendingOptions()
1256
    {
1257
        $allOptions = [];
1258
        $blindSettings = $this->settings['blind'];
1259
        if (!$blindSettings['descending']) {
1260
            $allOptions = [
1261
                0 => LocalizationUtility::translate('sortOrders.descending', 'IndexedSearch'),
1262
                1 => LocalizationUtility::translate('sortOrders.ascending', 'IndexedSearch')
1263
            ];
1264
        }
1265
        // disable single entries by TypoScript
1266
        $allOptions = $this->removeOptionsFromOptionList($allOptions, $blindSettings['descending.']);
1267
        return $allOptions;
1268
    }
1269
1270
    /**
1271
     * get the values for the "results" selector
1272
     *
1273
     * @return array Associative array with options
1274
     */
1275
    protected function getAllAvailableNumberOfResultsOptions()
1276
    {
1277
        $allOptions = [];
1278
        if (count($this->availableResultsNumbers) > 1) {
1279
            $allOptions = array_combine($this->availableResultsNumbers, $this->availableResultsNumbers) ?: [];
1280
        }
1281
        // disable single entries by TypoScript
1282
        $allOptions = $this->removeOptionsFromOptionList($allOptions, $this->settings['blind']['numberOfResults']);
1283
        return $allOptions;
1284
    }
1285
1286
    /**
1287
     * removes blinding entries from the option list of a selector
1288
     *
1289
     * @param array $allOptions associative array containing all options
1290
     * @param array $blindOptions associative array containing the optionkey as they key and the value = 1 if it should be removed
1291
     * @return array Options from $allOptions with some options removed
1292
     */
1293
    protected function removeOptionsFromOptionList($allOptions, $blindOptions)
1294
    {
1295
        if (is_array($blindOptions)) {
0 ignored issues
show
introduced by
The condition is_array($blindOptions) is always true.
Loading history...
1296
            foreach ($blindOptions as $key => $val) {
1297
                if ($val == 1) {
1298
                    unset($allOptions[$key]);
1299
                }
1300
            }
1301
        }
1302
        return $allOptions;
1303
    }
1304
1305
    /**
1306
     * Links the $linkText to page $pageUid
1307
     *
1308
     * @param int $pageUid Page id
1309
     * @param array $row Result row
1310
     * @param array $markUpSwParams Additional parameters for marking up search words
1311
     * @return array
1312
     */
1313
    protected function linkPage($pageUid, $row = [], $markUpSwParams = [])
1314
    {
1315
        $pageLanguage = GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('language', 'contentId', 0);
1316
        // Parameters for link
1317
        $urlParameters = [];
1318
        if ($row['static_page_arguments'] !== null) {
1319
            $urlParameters = json_decode($row['static_page_arguments'], true);
1320
        }
1321
        // Add &type and &MP variable:
1322
        if ($row['data_page_mp']) {
1323
            $urlParameters['MP'] = $row['data_page_mp'];
1324
        }
1325
        if (($pageLanguage === 0 && $row['sys_language_uid'] > 0) || $pageLanguage > 0) {
1326
            $urlParameters['L'] = (int)$row['sys_language_uid'];
1327
        }
1328
        // markup-GET vars:
1329
        $urlParameters = array_merge($urlParameters, $markUpSwParams);
1330
        // This will make sure that the path is retrieved if it hasn't been
1331
        // already. Used only for the sake of the domain_record thing.
1332
        if (!is_array($this->domainRecords[$pageUid])) {
1333
            $this->getPathFromPageId($pageUid);
1334
        }
1335
1336
        return $this->preparePageLink($pageUid, $row, $urlParameters);
1337
    }
1338
1339
    /**
1340
     * Return the menu of pages used for the selector.
1341
     *
1342
     * @param int $pageUid Page ID for which to return menu
1343
     * @return array Menu items (for making the section selector box)
1344
     */
1345
    protected function getMenuOfPages($pageUid)
1346
    {
1347
        $pageRepository = GeneralUtility::makeInstance(PageRepository::class);
1348
        if ($this->settings['displayLevelxAllTypes']) {
1349
            return $pageRepository->getMenuForPages([$pageUid]);
1350
        }
1351
        return $pageRepository->getMenu($pageUid);
1352
    }
1353
1354
    /**
1355
     * Returns the path to the page $id
1356
     *
1357
     * @param int $id Page ID
1358
     * @param string $pathMP Content of the MP (mount point) variable
1359
     * @return string Path (HTML-escaped)
1360
     */
1361
    protected function getPathFromPageId($id, $pathMP = '')
1362
    {
1363
        $identStr = $id . '|' . $pathMP;
1364
        if (!isset($this->pathCache[$identStr])) {
1365
            $this->requiredFrontendUsergroups[$id] = [];
1366
            $this->domainRecords[$id] = [];
1367
            try {
1368
                $rl = GeneralUtility::makeInstance(RootlineUtility::class, $id, $pathMP)->get();
1369
                $path = '';
1370
                $pageCount = count($rl);
1371
                if (!empty($rl)) {
1372
                    $excludeDoktypesFromPath = GeneralUtility::trimExplode(
1373
                        ',',
1374
                        $this->settings['results']['pathExcludeDoktypes'] ?? '',
1375
                        true
1376
                    );
1377
                    $breadcrumbWrap = $this->settings['breadcrumbWrap'] ?? '/';
1378
                    $breadcrumbWraps = GeneralUtility::makeInstance(TypoScriptService::class)
1379
                        ->explodeConfigurationForOptionSplit(['wrap' => $breadcrumbWrap], $pageCount);
1380
                    foreach ($rl as $k => $v) {
1381
                        if (in_array($v['doktype'], $excludeDoktypesFromPath, false)) {
1382
                            continue;
1383
                        }
1384
                        // Check fe_user
1385
                        if ($v['fe_group'] && ($v['uid'] == $id || $v['extendToSubpages'])) {
1386
                            $this->requiredFrontendUsergroups[$id][] = $v['fe_group'];
1387
                        }
1388
                        // Check sys_domain
1389
                        if ($this->settings['detectDomainRecords']) {
1390
                            $domainName = $this->getFirstDomainForPage((int)$v['uid']);
1391
                            if ($domainName) {
1392
                                $this->domainRecords[$id][] = $domainName;
1393
                                // Set path accordingly
1394
                                $path = $domainName . $path;
1395
                                break;
1396
                            }
1397
                        }
1398
                        // Stop, if we find that the current id is the current root page.
1399
                        if ($v['uid'] == $GLOBALS['TSFE']->config['rootLine'][0]['uid']) {
1400
                            array_pop($breadcrumbWraps);
1401
                            break;
1402
                        }
1403
                        $path = $GLOBALS['TSFE']->cObj->wrap(htmlspecialchars($v['title']), array_pop($breadcrumbWraps)['wrap']) . $path;
1404
                    }
1405
                }
1406
            } catch (RootLineException $e) {
1407
                $path = '';
1408
            }
1409
            $this->pathCache[$identStr] = $path;
1410
        }
1411
        return $this->pathCache[$identStr];
1412
    }
1413
1414
    /**
1415
     * Gets the first domain for the page
1416
     *
1417
     * @param int $id Page id
1418
     * @return string Domain name
1419
     */
1420
    protected function getFirstDomainForPage(int $id): string
1421
    {
1422
        $domain = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $domain is dead and can be removed.
Loading history...
1423
        try {
1424
            $domain = GeneralUtility::makeInstance(SiteFinder::class)
1425
                ->getSiteByRootPageId($id)
1426
                ->getBase()
1427
                ->getHost();
1428
        } catch (SiteNotFoundException $e) {
1429
            // site was not found, we return an empty string as default
1430
        }
1431
        return $domain;
1432
    }
1433
1434
    /**
1435
     * simple function to initialize possible external parsers
1436
     * feeds the $this->externalParsers array
1437
     */
1438
    protected function initializeExternalParsers()
1439
    {
1440
        // Initialize external document parsers for icon display and other soft operations
1441
        foreach ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['external_parsers'] ?? [] as $extension => $className) {
1442
            $this->externalParsers[$extension] = GeneralUtility::makeInstance($className);
1443
            // Init parser and if it returns FALSE, unset its entry again
1444
            if (!$this->externalParsers[$extension]->softInit($extension)) {
1445
                unset($this->externalParsers[$extension]);
1446
            }
1447
        }
1448
    }
1449
1450
    /**
1451
     * Returns an object reference to the hook object if any
1452
     *
1453
     * @param string $functionName Name of the function you want to call / hook key
1454
     * @return object|null Hook object, if any. Otherwise NULL.
1455
     */
1456
    protected function hookRequest($functionName)
1457
    {
1458
        // Hook: menuConfig_preProcessModMenu
1459
        if ($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]) {
1460
            $hookObj = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['indexed_search']['pi1_hooks'][$functionName]);
1461
            if (method_exists($hookObj, $functionName)) {
1462
                $hookObj->pObj = $this;
1463
                return $hookObj;
1464
            }
1465
        }
1466
        return null;
1467
    }
1468
1469
    /**
1470
     * Returns if an item type is a multipage item type
1471
     *
1472
     * @param string $item_type Item type
1473
     * @return bool TRUE if multipage capable
1474
     */
1475
    protected function multiplePagesType($item_type)
1476
    {
1477
        return is_object($this->externalParsers[$item_type]) && $this->externalParsers[$item_type]->isMultiplePageExtension($item_type);
1478
    }
1479
1480
    /**
1481
     * Process variables related to indexed_search extendedSearch needed by frontend view.
1482
     * Populate select boxes and setting some flags.
1483
     * The returned data can be passed directly into the view by assignMultiple()
1484
     *
1485
     * @return array Variables to pass into the view so they can be used in fluid template
1486
     */
1487
    protected function processExtendedSearchParameters()
1488
    {
1489
        $allSearchTypes = $this->getAllAvailableSearchTypeOptions();
1490
        $allDefaultOperands = $this->getAllAvailableOperandsOptions();
1491
        $allMediaTypes = $this->getAllAvailableMediaTypesOptions();
1492
        $allLanguageUids = $this->getAllAvailableLanguageOptions();
1493
        $allSortOrders = $this->getAllAvailableSortOrderOptions();
1494
        $allSortDescendings = $this->getAllAvailableSortDescendingOptions();
1495
1496
        return [
1497
            'allSearchTypes' => $allSearchTypes,
1498
            'allDefaultOperands' => $allDefaultOperands,
1499
            'showTypeSearch' => !empty($allSearchTypes) || !empty($allDefaultOperands),
1500
            'allMediaTypes' => $allMediaTypes,
1501
            'allLanguageUids' => $allLanguageUids,
1502
            'showMediaAndLanguageSearch' => !empty($allMediaTypes) || !empty($allLanguageUids),
1503
            'allSections' => $this->getAllAvailableSectionsOptions(),
1504
            'allIndexConfigurations' => $this->getAllAvailableIndexConfigurationsOptions(),
1505
            'allSortOrders' => $allSortOrders,
1506
            'allSortDescendings' => $allSortDescendings,
1507
            'showSortOrders' => !empty($allSortOrders) || !empty($allSortDescendings),
1508
            'allNumberOfResults' => $this->getAllAvailableNumberOfResultsOptions(),
1509
            'allGroups' => $this->getAllAvailableGroupOptions()
1510
        ];
1511
    }
1512
1513
    /**
1514
     * Load settings and apply stdWrap to them
1515
     */
1516
    protected function loadSettings()
1517
    {
1518
        if (!is_array($this->settings['results.'])) {
1519
            $this->settings['results.'] = [];
1520
        }
1521
        $fullTypoScriptArray = $this->typoScriptService->convertPlainArrayToTypoScriptArray($this->settings);
1522
        $this->settings['detectDomainRecords'] = $fullTypoScriptArray['detectDomainRecords'] ?? 0;
1523
        $this->settings['detectDomainRecords.'] = $fullTypoScriptArray['detectDomainRecords.'] ?? [];
1524
        $typoScriptArray = $fullTypoScriptArray['results.'];
1525
1526
        $this->settings['results.']['summaryCropAfter'] = MathUtility::forceIntegerInRange(
1527
            $GLOBALS['TSFE']->cObj->stdWrapValue('summaryCropAfter', $typoScriptArray ?? []),
1528
            10,
1529
            5000,
1530
            180
1531
        );
1532
        $this->settings['results.']['summaryCropSignifier'] = $GLOBALS['TSFE']->cObj->stdWrapValue('summaryCropSignifier', $typoScriptArray ?? []);
1533
        $this->settings['results.']['titleCropAfter'] = MathUtility::forceIntegerInRange(
1534
            $GLOBALS['TSFE']->cObj->stdWrapValue('titleCropAfter', $typoScriptArray ?? []),
1535
            10,
1536
            500,
1537
            50
1538
        );
1539
        $this->settings['results.']['titleCropSignifier'] = $GLOBALS['TSFE']->cObj->stdWrapValue('titleCropSignifier', $typoScriptArray ?? []);
1540
        $this->settings['results.']['markupSW_summaryMax'] = MathUtility::forceIntegerInRange(
1541
            $GLOBALS['TSFE']->cObj->stdWrapValue('markupSW_summaryMax', $typoScriptArray ?? []),
1542
            10,
1543
            5000,
1544
            300
1545
        );
1546
        $this->settings['results.']['markupSW_postPreLgd'] = MathUtility::forceIntegerInRange(
1547
            $GLOBALS['TSFE']->cObj->stdWrapValue('markupSW_postPreLgd', $typoScriptArray ?? []),
1548
            1,
1549
            500,
1550
            60
1551
        );
1552
        $this->settings['results.']['markupSW_postPreLgd_offset'] = MathUtility::forceIntegerInRange(
1553
            $GLOBALS['TSFE']->cObj->stdWrapValue('markupSW_postPreLgd_offset', $typoScriptArray ?? []),
1554
            1,
1555
            50,
1556
            5
1557
        );
1558
        $this->settings['results.']['markupSW_divider'] = $GLOBALS['TSFE']->cObj->stdWrapValue('markupSW_divider', $typoScriptArray ?? []);
1559
        $this->settings['results.']['hrefInSummaryCropAfter'] = MathUtility::forceIntegerInRange(
1560
            $GLOBALS['TSFE']->cObj->stdWrapValue('hrefInSummaryCropAfter', $typoScriptArray ?? []),
1561
            10,
1562
            400,
1563
            60
1564
        );
1565
        $this->settings['results.']['hrefInSummaryCropSignifier'] = $GLOBALS['TSFE']->cObj->stdWrapValue('hrefInSummaryCropSignifier', $typoScriptArray ?? []);
1566
    }
1567
1568
    /**
1569
     * Returns number of results to display
1570
     *
1571
     * @param int $numberOfResults Requested number of results
1572
     * @return int
1573
     */
1574
    protected function getNumberOfResults($numberOfResults)
1575
    {
1576
        $numberOfResults = (int)$numberOfResults;
1577
1578
        return in_array($numberOfResults, $this->availableResultsNumbers) ?
1579
            $numberOfResults : $this->defaultResultNumber;
1580
    }
1581
1582
    /**
1583
     * Internal method to build the page uri and link target.
1584
     * @todo make use of the UriBuilder
1585
     *
1586
     * @param int $pageUid
1587
     * @param array $row
1588
     * @param array $urlParameters
1589
     * @return array
1590
     */
1591
    protected function preparePageLink(int $pageUid, array $row, array $urlParameters): array
1592
    {
1593
        $target = '';
1594
        $uri = $this->controllerContext->getUriBuilder()
1595
                ->setTargetPageUid($pageUid)
1596
                ->setTargetPageType($row['data_page_type'])
1597
                ->setArguments($urlParameters)
1598
                ->build();
1599
1600
        // If external domain, then link to that:
1601
        if (!empty($this->domainRecords[$pageUid])) {
1602
            $scheme = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https://' : 'http://';
1603
            $firstDomain = reset($this->domainRecords[$pageUid]);
1604
            $uri = $scheme . $firstDomain . $uri;
1605
            $target = $this->settings['detectDomainRecords.']['target'] ?? '';
1606
        }
1607
1608
        return ['uri' => $uri, 'target' => $target];
1609
    }
1610
1611
    /**
1612
     * Create a tag for "path" key in search result
1613
     *
1614
     * @param string $linkText Link text (nodeValue)
1615
     * @param array $linkData
1616
     * @return string HTML <A> tag wrapped title string.
1617
     */
1618
    protected function linkPageATagWrap(string $linkText, array $linkData): string
1619
    {
1620
        $attributes = [
1621
            'href' => $linkData['uri']
1622
        ];
1623
        if (!empty($linkData['target'])) {
1624
            $attributes['target'] = $linkData['target'];
1625
        }
1626
        return sprintf(
1627
            '<a %s>%s</a>',
1628
            GeneralUtility::implodeAttributes($attributes, true),
1629
            htmlspecialchars($linkText, ENT_QUOTES | ENT_HTML5)
1630
        );
1631
    }
1632
1633
    /**
1634
     * Set the search word
1635
     * @param string $sword
1636
     */
1637
    public function setSword($sword)
1638
    {
1639
        $this->sword = (string)$sword;
1640
    }
1641
1642
    /**
1643
     * Returns the search word
1644
     * @return string
1645
     */
1646
    public function getSword()
1647
    {
1648
        return (string)$this->sword;
1649
    }
1650
}
1651