xref: /dokuwiki/inc/Ui/Search.php (revision 03fdedf74fd0e882fc06c06cd90a2bb608fe374b)
121fcef82SMichael Große<?php
221fcef82SMichael Große
321fcef82SMichael Großenamespace dokuwiki\Ui;
421fcef82SMichael Große
5cbb44eabSAndreas Gohruse dokuwiki\Extension\Event;
664159a61SAndreas Gohruse dokuwiki\Form\Form;
779a2d784SGerrit Uitslaguse dokuwiki\Utf8\PhpString;
82d85e841SAndreas Gohruse dokuwiki\Utf8\Sort;
9427ed988SMichael Große
1021fcef82SMichael Großeclass Search extends Ui
1121fcef82SMichael Große{
1221fcef82SMichael Große    protected $query;
134c924eb8SMichael Große    protected $parsedQuery;
1418856c5dSMichael Große    protected $searchState;
15e2d055f5SAndreas Gohr    protected $pageLookupResults = [];
16e2d055f5SAndreas Gohr    protected $fullTextResults = [];
17e2d055f5SAndreas Gohr    protected $highlight = [];
1821fcef82SMichael Große
1921fcef82SMichael Große    /**
2021fcef82SMichael Große     * Search constructor.
216639a152SMichael Große     *
22fc46ed58SMichael Große     * @param array $pageLookupResults pagename lookup results in the form [pagename => pagetitle]
23fc46ed58SMichael Große     * @param array $fullTextResults fulltext search results in the form [pagename => #hits]
24fc46ed58SMichael Große     * @param array $highlight array of strings to be highlighted
2521fcef82SMichael Große     */
266639a152SMichael Große    public function __construct(array $pageLookupResults, array $fullTextResults, $highlight)
2721fcef82SMichael Große    {
28d09b5b64SMichael Große        global $QUERY;
294c924eb8SMichael Große        $Indexer = idx_get_indexer();
30d09b5b64SMichael Große
31d09b5b64SMichael Große        $this->query = $QUERY;
32bbc1da2eSMichael Große        $this->parsedQuery = ft_queryParser($Indexer, $QUERY);
3318856c5dSMichael Große        $this->searchState = new SearchState($this->parsedQuery);
3421fcef82SMichael Große
356639a152SMichael Große        $this->pageLookupResults = $pageLookupResults;
366639a152SMichael Große        $this->fullTextResults = $fullTextResults;
3721fcef82SMichael Große        $this->highlight = $highlight;
38b3cfe85aSMichael Große    }
39bbc1da2eSMichael Große
40b3cfe85aSMichael Große    /**
4121fcef82SMichael Große     * display the search result
4221fcef82SMichael Große     *
4321fcef82SMichael Große     * @return void
4421fcef82SMichael Große     */
4521fcef82SMichael Große    public function show()
4621fcef82SMichael Große    {
4779a2d784SGerrit Uitslag        $searchHTML = $this->getSearchIntroHTML($this->query);
4821fcef82SMichael Große
492ce8affcSMichael Große        $searchHTML .= $this->getSearchFormHTML($this->query);
502ce8affcSMichael Große
5121fcef82SMichael Große        $searchHTML .= $this->getPageLookupHTML($this->pageLookupResults);
5221fcef82SMichael Große
5321fcef82SMichael Große        $searchHTML .= $this->getFulltextResultsHTML($this->fullTextResults, $this->highlight);
5421fcef82SMichael Große
5521fcef82SMichael Große        echo $searchHTML;
5621fcef82SMichael Große    }
5721fcef82SMichael Große
5821fcef82SMichael Große    /**
59427ed988SMichael Große     * Get a form which can be used to adjust/refine the search
60427ed988SMichael Große     *
61427ed988SMichael Große     * @param string $query
62427ed988SMichael Große     *
63427ed988SMichael Große     * @return string
64427ed988SMichael Große     */
65427ed988SMichael Große    protected function getSearchFormHTML($query)
66427ed988SMichael Große    {
67bbc1da2eSMichael Große        global $lang, $ID, $INPUT;
68427ed988SMichael Große
697fa270bcSMichael Große        $searchForm = (new Form(['method' => 'get'], true))->addClass('search-results-form');
70bb8ef867SMichael Große        $searchForm->setHiddenField('do', 'search');
71d22b78c8SMichael Große        $searchForm->setHiddenField('id', $ID);
721265b193SMichael Große        $searchForm->setHiddenField('sf', '1');
73422bbbc6SMichael Große        if ($INPUT->has('min')) {
74422bbbc6SMichael Große            $searchForm->setHiddenField('min', $INPUT->str('min'));
75bbc1da2eSMichael Große        }
76422bbbc6SMichael Große        if ($INPUT->has('max')) {
77422bbbc6SMichael Große            $searchForm->setHiddenField('max', $INPUT->str('max'));
78bbc1da2eSMichael Große        }
791265b193SMichael Große        if ($INPUT->has('srt')) {
801265b193SMichael Große            $searchForm->setHiddenField('srt', $INPUT->str('srt'));
818d0e286aSMichael Große        }
824bdf82b5SAndreas Gohr        $searchForm->addFieldsetOpen()->addClass('search-form');
83d22b78c8SMichael Große        $searchForm->addTextInput('q')->val($query)->useInput(false);
84427ed988SMichael Große        $searchForm->addButton('', $lang['btn_search'])->attr('type', 'submit');
85bb8ef867SMichael Große
8618856c5dSMichael Große        $this->addSearchAssistanceElements($searchForm);
87bb8ef867SMichael Große
88427ed988SMichael Große        $searchForm->addFieldsetClose();
89427ed988SMichael Große
90c6977b3aSSatoshi Sahara        return $searchForm->toHTML('Search');
91427ed988SMichael Große    }
92427ed988SMichael Große
93be76738bSMichael Große    /**
94be76738bSMichael Große     * Add elements to adjust how the results are sorted
95be76738bSMichael Große     *
96be76738bSMichael Große     * @param Form $searchForm
97be76738bSMichael Große     */
98b005809cSMichael Große    protected function addSortTool(Form $searchForm)
99b005809cSMichael Große    {
100b005809cSMichael Große        global $INPUT, $lang;
101b005809cSMichael Große
102b005809cSMichael Große        $options = [
103b005809cSMichael Große            'hits' => [
104b005809cSMichael Große                'label' => $lang['search_sort_by_hits'],
105b005809cSMichael Große                'sort' => '',
106b005809cSMichael Große            ],
107b005809cSMichael Große            'mtime' => [
108b005809cSMichael Große                'label' => $lang['search_sort_by_mtime'],
109b005809cSMichael Große                'sort' => 'mtime',
110b005809cSMichael Große            ],
111b005809cSMichael Große        ];
112b005809cSMichael Große        $activeOption = 'hits';
113b005809cSMichael Große
1141265b193SMichael Große        if ($INPUT->str('srt') === 'mtime') {
115b005809cSMichael Große            $activeOption = 'mtime';
116b005809cSMichael Große        }
117b005809cSMichael Große
1182171f9cbSAndreas Gohr        $searchForm->addTagOpen('div')->addClass('toggle')->attr('aria-haspopup', 'true');
119b005809cSMichael Große        // render current
1204bdf82b5SAndreas Gohr        $currentWrapper = $searchForm->addTagOpen('div')->addClass('current');
121b005809cSMichael Große        if ($activeOption !== 'hits') {
1224bdf82b5SAndreas Gohr            $currentWrapper->addClass('changed');
123b005809cSMichael Große        }
124b005809cSMichael Große        $searchForm->addHTML($options[$activeOption]['label']);
125b005809cSMichael Große        $searchForm->addTagClose('div');
126b005809cSMichael Große
127b005809cSMichael Große        // render options list
1282171f9cbSAndreas Gohr        $searchForm->addTagOpen('ul')->attr('aria-expanded', 'false');
129b005809cSMichael Große
130b005809cSMichael Große        foreach ($options as $key => $option) {
1314bdf82b5SAndreas Gohr            $listItem = $searchForm->addTagOpen('li');
132b005809cSMichael Große
133b005809cSMichael Große            if ($key === $activeOption) {
1344bdf82b5SAndreas Gohr                $listItem->addClass('active');
135b005809cSMichael Große                $searchForm->addHTML($option['label']);
136b005809cSMichael Große            } else {
13752d4cd42SMichael Große                $link = $this->searchState->withSorting($option['sort'])->getSearchLink($option['label']);
13852d4cd42SMichael Große                $searchForm->addHTML($link);
139b005809cSMichael Große            }
140b005809cSMichael Große            $searchForm->addTagClose('li');
141b005809cSMichael Große        }
142b005809cSMichael Große        $searchForm->addTagClose('ul');
143b005809cSMichael Große
144b005809cSMichael Große        $searchForm->addTagClose('div');
145b005809cSMichael Große    }
146b005809cSMichael Große
147be76738bSMichael Große    /**
148be76738bSMichael Große     * Check if the query is simple enough to modify its namespace limitations without breaking the rest of the query
149be76738bSMichael Große     *
150be76738bSMichael Große     * @param array $parsedQuery
151be76738bSMichael Große     *
152be76738bSMichael Große     * @return bool
153be76738bSMichael Große     */
154e2d055f5SAndreas Gohr    protected function isNamespaceAssistanceAvailable(array $parsedQuery)
155e2d055f5SAndreas Gohr    {
156df977249SMichael Große        if (preg_match('/[\(\)\|]/', $parsedQuery['query']) === 1) {
157bb8ef867SMichael Große            return false;
158bb8ef867SMichael Große        }
159df977249SMichael Große
160df977249SMichael Große        return true;
161df977249SMichael Große    }
162df977249SMichael Große
163be76738bSMichael Große    /**
164be76738bSMichael Große     * Check if the query is simple enough to modify the fragment search behavior without breaking the rest of the query
165be76738bSMichael Große     *
166be76738bSMichael Große     * @param array $parsedQuery
167be76738bSMichael Große     *
168be76738bSMichael Große     * @return bool
169be76738bSMichael Große     */
170e2d055f5SAndreas Gohr    protected function isFragmentAssistanceAvailable(array $parsedQuery)
171e2d055f5SAndreas Gohr    {
172df977249SMichael Große        if (preg_match('/[\(\)\|]/', $parsedQuery['query']) === 1) {
173bb8ef867SMichael Große            return false;
174bb8ef867SMichael Große        }
175bb8ef867SMichael Große
176bb8ef867SMichael Große        if (!empty($parsedQuery['phrases'])) {
177bb8ef867SMichael Große            return false;
178bb8ef867SMichael Große        }
179bb8ef867SMichael Große
180bb8ef867SMichael Große        return true;
181bb8ef867SMichael Große    }
182bb8ef867SMichael Große
183bb8ef867SMichael Große    /**
184bb8ef867SMichael Große     * Add the elements to be used for search assistance
185bb8ef867SMichael Große     *
186bb8ef867SMichael Große     * @param Form $searchForm
187bb8ef867SMichael Große     */
18818856c5dSMichael Große    protected function addSearchAssistanceElements(Form $searchForm)
189bb8ef867SMichael Große    {
190bb8ef867SMichael Große        $searchForm->addTagOpen('div')
1914bdf82b5SAndreas Gohr            ->addClass('advancedOptions')
1922171f9cbSAndreas Gohr            ->attr('style', 'display: none;')
1932171f9cbSAndreas Gohr            ->attr('aria-hidden', 'true');
194bb8ef867SMichael Große
19518856c5dSMichael Große        $this->addFragmentBehaviorLinks($searchForm);
19618856c5dSMichael Große        $this->addNamespaceSelector($searchForm);
19718856c5dSMichael Große        $this->addDateSelector($searchForm);
198b005809cSMichael Große        $this->addSortTool($searchForm);
199bb8ef867SMichael Große
200bb8ef867SMichael Große        $searchForm->addTagClose('div');
201bb8ef867SMichael Große    }
202bb8ef867SMichael Große
203be76738bSMichael Große    /**
204be76738bSMichael Große     *  Add the elements to adjust the fragment search behavior
205be76738bSMichael Große     *
206be76738bSMichael Große     * @param Form $searchForm
207be76738bSMichael Große     */
20818856c5dSMichael Große    protected function addFragmentBehaviorLinks(Form $searchForm)
2094d0cb6e1SMichael Große    {
210df977249SMichael Große        if (!$this->isFragmentAssistanceAvailable($this->parsedQuery)) {
211df977249SMichael Große            return;
212df977249SMichael Große        }
213b005809cSMichael Große        global $lang;
2144d0cb6e1SMichael Große
215b005809cSMichael Große        $options = [
216b005809cSMichael Große            'exact' => [
217b005809cSMichael Große                'label' => $lang['search_exact_match'],
218e2d055f5SAndreas Gohr                'and' => array_map(static fn($term) => trim($term, '*'), $this->parsedQuery['and']),
219e2d055f5SAndreas Gohr                'not' => array_map(static fn($term) => trim($term, '*'), $this->parsedQuery['not']),
220b005809cSMichael Große            ],
221b005809cSMichael Große            'starts' => [
222b005809cSMichael Große                'label' => $lang['search_starts_with'],
223e2d055f5SAndreas Gohr                'and' => array_map(static fn($term) => trim($term, '*') . '*', $this->parsedQuery['and']),
224e2d055f5SAndreas Gohr                'not' => array_map(static fn($term) => trim($term, '*') . '*', $this->parsedQuery['not']),
225b005809cSMichael Große            ],
226b005809cSMichael Große            'ends' => [
227b005809cSMichael Große                'label' => $lang['search_ends_with'],
228e2d055f5SAndreas Gohr                'and' => array_map(static fn($term) => '*' . trim($term, '*'), $this->parsedQuery['and']),
229e2d055f5SAndreas Gohr                'not' => array_map(static fn($term) => '*' . trim($term, '*'), $this->parsedQuery['not']),
230b005809cSMichael Große            ],
231b005809cSMichael Große            'contains' => [
232b005809cSMichael Große                'label' => $lang['search_contains'],
233e2d055f5SAndreas Gohr                'and' => array_map(static fn($term) => '*' . trim($term, '*') . '*', $this->parsedQuery['and']),
234e2d055f5SAndreas Gohr                'not' => array_map(static fn($term) => '*' . trim($term, '*') . '*', $this->parsedQuery['not']),
235b005809cSMichael Große            ]
236b005809cSMichael Große        ];
237b005809cSMichael Große
238b005809cSMichael Große        // detect current
239c6b5b74aSMichael Große        $activeOption = 'custom';
240b005809cSMichael Große        foreach ($options as $key => $option) {
241b005809cSMichael Große            if ($this->parsedQuery['and'] === $option['and']) {
242b005809cSMichael Große                $activeOption = $key;
243b005809cSMichael Große            }
244b005809cSMichael Große        }
245c6b5b74aSMichael Große        if ($activeOption === 'custom') {
246c6b5b74aSMichael Große            $options = array_merge(['custom' => [
247c6b5b74aSMichael Große                'label' => $lang['search_custom_match'],
248c6b5b74aSMichael Große            ]], $options);
249c6b5b74aSMichael Große        }
250b005809cSMichael Große
2512171f9cbSAndreas Gohr        $searchForm->addTagOpen('div')->addClass('toggle')->attr('aria-haspopup', 'true');
252b005809cSMichael Große        // render current
2534bdf82b5SAndreas Gohr        $currentWrapper = $searchForm->addTagOpen('div')->addClass('current');
254b005809cSMichael Große        if ($activeOption !== 'exact') {
2554bdf82b5SAndreas Gohr            $currentWrapper->addClass('changed');
256b005809cSMichael Große        }
257b005809cSMichael Große        $searchForm->addHTML($options[$activeOption]['label']);
258b005809cSMichael Große        $searchForm->addTagClose('div');
259b005809cSMichael Große
260b005809cSMichael Große        // render options list
2612171f9cbSAndreas Gohr        $searchForm->addTagOpen('ul')->attr('aria-expanded', 'false');
262b005809cSMichael Große
263b005809cSMichael Große        foreach ($options as $key => $option) {
2644bdf82b5SAndreas Gohr            $listItem = $searchForm->addTagOpen('li');
265b005809cSMichael Große
266b005809cSMichael Große            if ($key === $activeOption) {
2674bdf82b5SAndreas Gohr                $listItem->addClass('active');
268b005809cSMichael Große                $searchForm->addHTML($option['label']);
269b005809cSMichael Große            } else {
27052d4cd42SMichael Große                $link = $this->searchState
27152d4cd42SMichael Große                    ->withFragments($option['and'], $option['not'])
272e2d055f5SAndreas Gohr                    ->getSearchLink($option['label']);
27352d4cd42SMichael Große                $searchForm->addHTML($link);
274b005809cSMichael Große            }
275b005809cSMichael Große            $searchForm->addTagClose('li');
276b005809cSMichael Große        }
277b005809cSMichael Große        $searchForm->addTagClose('ul');
2784d0cb6e1SMichael Große
2794d0cb6e1SMichael Große        $searchForm->addTagClose('div');
280b005809cSMichael Große
281b005809cSMichael Große        // render options list
2824d0cb6e1SMichael Große    }
2834d0cb6e1SMichael Große
284bb8ef867SMichael Große    /**
285bb8ef867SMichael Große     * Add the elements for the namespace selector
286bb8ef867SMichael Große     *
287bb8ef867SMichael Große     * @param Form $searchForm
288bb8ef867SMichael Große     */
28918856c5dSMichael Große    protected function addNamespaceSelector(Form $searchForm)
290bb8ef867SMichael Große    {
291df977249SMichael Große        if (!$this->isNamespaceAssistanceAvailable($this->parsedQuery)) {
292df977249SMichael Große            return;
293df977249SMichael Große        }
294df977249SMichael Große
295b005809cSMichael Große        global $lang;
296b005809cSMichael Große
29718856c5dSMichael Große        $baseNS = empty($this->parsedQuery['ns']) ? '' : $this->parsedQuery['ns'][0];
298bbc1da2eSMichael Große        $extraNS = $this->getAdditionalNamespacesFromResults($baseNS);
2994d0cb6e1SMichael Große
3002171f9cbSAndreas Gohr        $searchForm->addTagOpen('div')->addClass('toggle')->attr('aria-haspopup', 'true');
301b005809cSMichael Große        // render current
3024bdf82b5SAndreas Gohr        $currentWrapper = $searchForm->addTagOpen('div')->addClass('current');
303bbc1da2eSMichael Große        if ($baseNS) {
3044bdf82b5SAndreas Gohr            $currentWrapper->addClass('changed');
305*03fdedf7SAndreas Gohr            $searchForm->addHTML('@' . hsc($baseNS));
306b005809cSMichael Große        } else {
307b005809cSMichael Große            $searchForm->addHTML($lang['search_any_ns']);
308b005809cSMichael Große        }
309b005809cSMichael Große        $searchForm->addTagClose('div');
310b005809cSMichael Große
311b005809cSMichael Große        // render options list
3122171f9cbSAndreas Gohr        $searchForm->addTagOpen('ul')->attr('aria-expanded', 'false');
313b005809cSMichael Große
3144bdf82b5SAndreas Gohr        $listItem = $searchForm->addTagOpen('li');
315b005809cSMichael Große        if ($baseNS) {
3164bdf82b5SAndreas Gohr            $listItem->addClass('active');
31752d4cd42SMichael Große            $link = $this->searchState->withNamespace('')->getSearchLink($lang['search_any_ns']);
31852d4cd42SMichael Große            $searchForm->addHTML($link);
319b005809cSMichael Große        } else {
320b005809cSMichael Große            $searchForm->addHTML($lang['search_any_ns']);
321bb8ef867SMichael Große        }
322b005809cSMichael Große        $searchForm->addTagClose('li');
323bb8ef867SMichael Große
32418856c5dSMichael Große        foreach ($extraNS as $ns => $count) {
3254bdf82b5SAndreas Gohr            $listItem = $searchForm->addTagOpen('li');
326*03fdedf7SAndreas Gohr            $label = hsc($ns) . ($count ? " <bdi>($count)</bdi>" : '');
3274d0cb6e1SMichael Große
328b005809cSMichael Große            if ($ns === $baseNS) {
3294bdf82b5SAndreas Gohr                $listItem->addClass('active');
330b005809cSMichael Große                $searchForm->addHTML($label);
331b005809cSMichael Große            } else {
33252d4cd42SMichael Große                $link = $this->searchState->withNamespace($ns)->getSearchLink($label);
33352d4cd42SMichael Große                $searchForm->addHTML($link);
334bb8ef867SMichael Große            }
335b005809cSMichael Große            $searchForm->addTagClose('li');
336bb8ef867SMichael Große        }
337b005809cSMichael Große        $searchForm->addTagClose('ul');
338bb8ef867SMichael Große
339bb8ef867SMichael Große        $searchForm->addTagClose('div');
340bb8ef867SMichael Große    }
341bb8ef867SMichael Große
342bb8ef867SMichael Große    /**
343bb8ef867SMichael Große     * Parse the full text results for their top namespaces below the given base namespace
344bb8ef867SMichael Große     *
345bb8ef867SMichael Große     * @param string $baseNS the namespace within which was searched, empty string for root namespace
346bb8ef867SMichael Große     *
347bb8ef867SMichael Große     * @return array an associative array with namespace => #number of found pages, sorted descending
348bb8ef867SMichael Große     */
349bb8ef867SMichael Große    protected function getAdditionalNamespacesFromResults($baseNS)
350bb8ef867SMichael Große    {
351bb8ef867SMichael Große        $namespaces = [];
352bb8ef867SMichael Große        $baseNSLength = strlen($baseNS);
353bb8ef867SMichael Große        foreach ($this->fullTextResults as $page => $numberOfHits) {
354bb8ef867SMichael Große            $namespace = getNS($page);
355bb8ef867SMichael Große            if (!$namespace) {
356bb8ef867SMichael Große                continue;
357bb8ef867SMichael Große            }
358bb8ef867SMichael Große            if ($namespace === $baseNS) {
359bb8ef867SMichael Große                continue;
360bb8ef867SMichael Große            }
361bb8ef867SMichael Große            $firstColon = strpos((string)$namespace, ':', $baseNSLength + 1) ?: strlen($namespace);
362bb8ef867SMichael Große            $subtopNS = substr($namespace, 0, $firstColon);
363bb8ef867SMichael Große            if (empty($namespaces[$subtopNS])) {
364bb8ef867SMichael Große                $namespaces[$subtopNS] = 0;
365bb8ef867SMichael Große            }
366e2d055f5SAndreas Gohr            ++$namespaces[$subtopNS];
367bb8ef867SMichael Große        }
3682d85e841SAndreas Gohr        Sort::ksort($namespaces);
369bb8ef867SMichael Große        arsort($namespaces);
370bb8ef867SMichael Große        return $namespaces;
371bb8ef867SMichael Große    }
372bb8ef867SMichael Große
373bb8ef867SMichael Große    /**
374bbc1da2eSMichael Große     * @ToDo: custom date input
375bbc1da2eSMichael Große     *
376bbc1da2eSMichael Große     * @param Form $searchForm
377bbc1da2eSMichael Große     */
378b005809cSMichael Große    protected function addDateSelector(Form $searchForm)
379b005809cSMichael Große    {
380b005809cSMichael Große        global $INPUT, $lang;
381bbc1da2eSMichael Große
382b005809cSMichael Große        $options = [
383b005809cSMichael Große            'any' => [
384b005809cSMichael Große                'before' => false,
385b005809cSMichael Große                'after' => false,
386b005809cSMichael Große                'label' => $lang['search_any_time'],
387b005809cSMichael Große            ],
388b005809cSMichael Große            'week' => [
389b005809cSMichael Große                'before' => false,
390b005809cSMichael Große                'after' => '1 week ago',
391b005809cSMichael Große                'label' => $lang['search_past_7_days'],
392b005809cSMichael Große            ],
393b005809cSMichael Große            'month' => [
394b005809cSMichael Große                'before' => false,
395b005809cSMichael Große                'after' => '1 month ago',
396b005809cSMichael Große                'label' => $lang['search_past_month'],
397b005809cSMichael Große            ],
398b005809cSMichael Große            'year' => [
399b005809cSMichael Große                'before' => false,
400b005809cSMichael Große                'after' => '1 year ago',
401b005809cSMichael Große                'label' => $lang['search_past_year'],
402b005809cSMichael Große            ],
403b005809cSMichael Große        ];
404b005809cSMichael Große        $activeOption = 'any';
405b005809cSMichael Große        foreach ($options as $key => $option) {
406422bbbc6SMichael Große            if ($INPUT->str('min') === $option['after']) {
407b005809cSMichael Große                $activeOption = $key;
408b005809cSMichael Große                break;
409b005809cSMichael Große            }
410b005809cSMichael Große        }
411b005809cSMichael Große
4122171f9cbSAndreas Gohr        $searchForm->addTagOpen('div')->addClass('toggle')->attr('aria-haspopup', 'true');
413b005809cSMichael Große        // render current
4144bdf82b5SAndreas Gohr        $currentWrapper = $searchForm->addTagOpen('div')->addClass('current');
415422bbbc6SMichael Große        if ($INPUT->has('max') || $INPUT->has('min')) {
4164bdf82b5SAndreas Gohr            $currentWrapper->addClass('changed');
417bbc1da2eSMichael Große        }
418b005809cSMichael Große        $searchForm->addHTML($options[$activeOption]['label']);
419b005809cSMichael Große        $searchForm->addTagClose('div');
420bbc1da2eSMichael Große
421b005809cSMichael Große        // render options list
4222171f9cbSAndreas Gohr        $searchForm->addTagOpen('ul')->attr('aria-expanded', 'false');
423b005809cSMichael Große
424b005809cSMichael Große        foreach ($options as $key => $option) {
4254bdf82b5SAndreas Gohr            $listItem = $searchForm->addTagOpen('li');
426b005809cSMichael Große
427b005809cSMichael Große            if ($key === $activeOption) {
4284bdf82b5SAndreas Gohr                $listItem->addClass('active');
429b005809cSMichael Große                $searchForm->addHTML($option['label']);
430bbc1da2eSMichael Große            } else {
43152d4cd42SMichael Große                $link = $this->searchState
43252d4cd42SMichael Große                    ->withTimeLimitations($option['after'], $option['before'])
433e2d055f5SAndreas Gohr                    ->getSearchLink($option['label']);
43452d4cd42SMichael Große                $searchForm->addHTML($link);
435bbc1da2eSMichael Große            }
436b005809cSMichael Große            $searchForm->addTagClose('li');
437bbc1da2eSMichael Große        }
438b005809cSMichael Große        $searchForm->addTagClose('ul');
439bbc1da2eSMichael Große
440bbc1da2eSMichael Große        $searchForm->addTagClose('div');
441bbc1da2eSMichael Große    }
442bbc1da2eSMichael Große
443bbc1da2eSMichael Große
444bbc1da2eSMichael Große    /**
44521fcef82SMichael Große     * Build the intro text for the search page
44621fcef82SMichael Große     *
44721fcef82SMichael Große     * @param string $query the search query
44821fcef82SMichael Große     *
44921fcef82SMichael Große     * @return string
45021fcef82SMichael Große     */
45121fcef82SMichael Große    protected function getSearchIntroHTML($query)
45221fcef82SMichael Große    {
4532ce8affcSMichael Große        global $lang;
45421fcef82SMichael Große
45521fcef82SMichael Große        $intro = p_locale_xhtml('searchpage');
4562ce8affcSMichael Große
4572ce8affcSMichael Große        $queryPagename = $this->createPagenameFromQuery($this->parsedQuery);
4582ce8affcSMichael Große        $createQueryPageLink = html_wikilink($queryPagename . '?do=edit', $queryPagename);
4592ce8affcSMichael Große
4602ce8affcSMichael Große        $pagecreateinfo = '';
4612ce8affcSMichael Große        if (auth_quickaclcheck($queryPagename) >= AUTH_CREATE) {
4622ce8affcSMichael Große            $pagecreateinfo = sprintf($lang['searchcreatepage'], $createQueryPageLink);
4632ce8affcSMichael Große        }
46479a2d784SGerrit Uitslag        return str_replace(
465e2d055f5SAndreas Gohr            ['@QUERY@', '@SEARCH@', '@CREATEPAGEINFO@'],
466e2d055f5SAndreas Gohr            [hsc(rawurlencode($query)), hsc($query), $pagecreateinfo],
46721fcef82SMichael Große            $intro
46821fcef82SMichael Große        );
46921fcef82SMichael Große    }
47021fcef82SMichael Große
47121fcef82SMichael Große    /**
4722ce8affcSMichael Große     * Create a pagename based the parsed search query
4732ce8affcSMichael Große     *
4742ce8affcSMichael Große     * @param array $parsedQuery
4752ce8affcSMichael Große     *
4762ce8affcSMichael Große     * @return string pagename constructed from the parsed query
4772ce8affcSMichael Große     */
47842690e4dSMichael Große    public function createPagenameFromQuery($parsedQuery)
4792ce8affcSMichael Große    {
480e180e453SPhy        $cleanedQuery = cleanID($parsedQuery['query']); // already strtolowered
48179a2d784SGerrit Uitslag        if ($cleanedQuery === PhpString::strtolower($parsedQuery['query'])) {
48242690e4dSMichael Große            return ':' . $cleanedQuery;
48342690e4dSMichael Große        }
4842ce8affcSMichael Große        $pagename = '';
4852ce8affcSMichael Große        if (!empty($parsedQuery['ns'])) {
48642690e4dSMichael Große            $pagename .= ':' . cleanID($parsedQuery['ns'][0]);
4872ce8affcSMichael Große        }
4882ce8affcSMichael Große        $pagename .= ':' . cleanID(implode(' ', $parsedQuery['highlight']));
4892ce8affcSMichael Große        return $pagename;
4902ce8affcSMichael Große    }
4912ce8affcSMichael Große
4922ce8affcSMichael Große    /**
49321fcef82SMichael Große     * Build HTML for a list of pages with matching pagenames
49421fcef82SMichael Große     *
49521fcef82SMichael Große     * @param array $data search results
49621fcef82SMichael Große     *
49721fcef82SMichael Große     * @return string
49821fcef82SMichael Große     */
49921fcef82SMichael Große    protected function getPageLookupHTML($data)
50021fcef82SMichael Große    {
50121fcef82SMichael Große        if (empty($data)) {
50221fcef82SMichael Große            return '';
50321fcef82SMichael Große        }
50421fcef82SMichael Große
50521fcef82SMichael Große        global $lang;
50621fcef82SMichael Große
50721fcef82SMichael Große        $html = '<div class="search_quickresult">';
5086d55fda7SMichael Große        $html .= '<h2>' . $lang['quickhits'] . ':</h2>';
50921fcef82SMichael Große        $html .= '<ul class="search_quickhits">';
510e2d055f5SAndreas Gohr        foreach (array_keys($data) as $id) {
5115d87aa31SMichael Große            $name = null;
5125d87aa31SMichael Große            if (!useHeading('navigation') && $ns = getNS($id)) {
5135d87aa31SMichael Große                $name = shorten(noNS($id), ' (' . $ns . ')', 30);
5145d87aa31SMichael Große            }
5155d87aa31SMichael Große            $link = html_wikilink(':' . $id, $name);
5164eab6f7cSMichael Große            $eventData = [
5174eab6f7cSMichael Große                'listItemContent' => [$link],
5184eab6f7cSMichael Große                'page' => $id,
5194eab6f7cSMichael Große            ];
520cbb44eabSAndreas Gohr            Event::createAndTrigger('SEARCH_RESULT_PAGELOOKUP', $eventData);
5214eab6f7cSMichael Große            $html .= '<li>' . implode('', $eventData['listItemContent']) . '</li>';
52221fcef82SMichael Große        }
52321fcef82SMichael Große        $html .= '</ul> ';
52421fcef82SMichael Große        //clear float (see http://www.complexspiral.com/publications/containing-floats/)
52521fcef82SMichael Große        $html .= '<div class="clearer"></div>';
52621fcef82SMichael Große        $html .= '</div>';
52721fcef82SMichael Große
52821fcef82SMichael Große        return $html;
52921fcef82SMichael Große    }
53021fcef82SMichael Große
53121fcef82SMichael Große    /**
53221fcef82SMichael Große     * Build HTML for fulltext search results or "no results" message
53321fcef82SMichael Große     *
53421fcef82SMichael Große     * @param array $data the results of the fulltext search
53521fcef82SMichael Große     * @param array $highlight the terms to be highlighted in the results
53621fcef82SMichael Große     *
53721fcef82SMichael Große     * @return string
53821fcef82SMichael Große     */
53921fcef82SMichael Große    protected function getFulltextResultsHTML($data, $highlight)
54021fcef82SMichael Große    {
54121fcef82SMichael Große        global $lang;
54221fcef82SMichael Große
54321fcef82SMichael Große        if (empty($data)) {
54421fcef82SMichael Große            return '<div class="nothing">' . $lang['nothingfound'] . '</div>';
54521fcef82SMichael Große        }
54621fcef82SMichael Große
5472ce8affcSMichael Große        $html = '<div class="search_fulltextresult">';
5486d55fda7SMichael Große        $html .= '<h2>' . $lang['search_fullresults'] . ':</h2>';
5492ce8affcSMichael Große
55021fcef82SMichael Große        $html .= '<dl class="search_results">';
5518225e1abSMichael Große        $num = 0;
5528225e1abSMichael Große        $position = 0;
5534c924eb8SMichael Große
55421fcef82SMichael Große        foreach ($data as $id => $cnt) {
555e2d055f5SAndreas Gohr            ++$position;
5564eab6f7cSMichael Große            $resultLink = html_wikilink(':' . $id, null, $highlight);
5574c924eb8SMichael Große
5584c924eb8SMichael Große            $resultHeader = [$resultLink];
5594c924eb8SMichael Große
5604eab6f7cSMichael Große
5614c924eb8SMichael Große            $restrictQueryToNSLink = $this->restrictQueryToNSLink(getNS($id));
5624c924eb8SMichael Große            if ($restrictQueryToNSLink) {
5634c924eb8SMichael Große                $resultHeader[] = $restrictQueryToNSLink;
5644c924eb8SMichael Große            }
5654c924eb8SMichael Große
5665d06a1e4SMichael Große            $resultBody = [];
5679a75abfbSMichael Große            $mtime = filemtime(wikiFN($id));
5685d06a1e4SMichael Große            $lastMod = '<span class="lastmod">' . $lang['lastmod'] . '</span> ';
56964159a61SAndreas Gohr            $lastMod .= '<time datetime="' . date_iso8601($mtime) . '" title="' . dformat($mtime) . '">' .
57064159a61SAndreas Gohr                dformat($mtime, '%f') .
57164159a61SAndreas Gohr                '</time>';
572f0861d1fSMichael Große            $resultBody['meta'] = $lastMod;
5735d06a1e4SMichael Große            if ($cnt !== 0) {
5748225e1abSMichael Große                $num++;
575b12bcb77SAnika Henke                $hits = '<span class="hits">' . $cnt . ' ' . $lang['hits'] . '</span>, ';
576f0861d1fSMichael Große                $resultBody['meta'] = $hits . $resultBody['meta'];
5778225e1abSMichael Große                if ($num <= FT_SNIPPET_NUMBER) { // create snippets for the first number of matches only
578f0861d1fSMichael Große                    $resultBody['snippet'] = ft_snippet($id, $highlight);
5799a75abfbSMichael Große                }
5809a75abfbSMichael Große            }
5819a75abfbSMichael Große
5824eab6f7cSMichael Große            $eventData = [
5834c924eb8SMichael Große                'resultHeader' => $resultHeader,
5845d06a1e4SMichael Große                'resultBody' => $resultBody,
5854eab6f7cSMichael Große                'page' => $id,
58678d786c9SMichael Große                'position' => $position,
5874eab6f7cSMichael Große            ];
588cbb44eabSAndreas Gohr            Event::createAndTrigger('SEARCH_RESULT_FULLPAGE', $eventData);
5895d06a1e4SMichael Große            $html .= '<div class="search_fullpage_result">';
5904eab6f7cSMichael Große            $html .= '<dt>' . implode(' ', $eventData['resultHeader']) . '</dt>';
5915d06a1e4SMichael Große            foreach ($eventData['resultBody'] as $class => $htmlContent) {
5925d06a1e4SMichael Große                $html .= "<dd class=\"$class\">$htmlContent</dd>";
5935d06a1e4SMichael Große            }
5945d06a1e4SMichael Große            $html .= '</div>';
59521fcef82SMichael Große        }
59621fcef82SMichael Große        $html .= '</dl>';
59721fcef82SMichael Große
5982ce8affcSMichael Große        $html .= '</div>';
5992ce8affcSMichael Große
60021fcef82SMichael Große        return $html;
60121fcef82SMichael Große    }
6024c924eb8SMichael Große
6034c924eb8SMichael Große    /**
6044c924eb8SMichael Große     * create a link to restrict the current query to a namespace
6054c924eb8SMichael Große     *
606ec27794fSMichael Große     * @param false|string $ns the namespace to which to restrict the query
6074c924eb8SMichael Große     *
608ec27794fSMichael Große     * @return false|string
6094c924eb8SMichael Große     */
6104c924eb8SMichael Große    protected function restrictQueryToNSLink($ns)
6114c924eb8SMichael Große    {
6124c924eb8SMichael Große        if (!$ns) {
6134c924eb8SMichael Große            return false;
6144c924eb8SMichael Große        }
615df977249SMichael Große        if (!$this->isNamespaceAssistanceAvailable($this->parsedQuery)) {
6164c924eb8SMichael Große            return false;
6174c924eb8SMichael Große        }
6184c924eb8SMichael Große        if (!empty($this->parsedQuery['ns']) && $this->parsedQuery['ns'][0] === $ns) {
6194c924eb8SMichael Große            return false;
6204c924eb8SMichael Große        }
62152d4cd42SMichael Große
6224c924eb8SMichael Große        $name = '@' . $ns;
62352d4cd42SMichael Große        return $this->searchState->withNamespace($ns)->getSearchLink($name);
6244c924eb8SMichael Große    }
62521fcef82SMichael Große}
626