Completed
Push — betterCoreSearch ( 940f24...01c23b )
by Michael
06:20
created

Search::addSortTool()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 52
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 32
nc 12
nop 1
dl 0
loc 52
rs 8.6868
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace dokuwiki\Ui;
4
5
use \dokuwiki\Form\Form;
6
7
class Search extends Ui
8
{
9
    protected $query;
10
    protected $parsedQuery;
11
    protected $searchState;
12
    protected $pageLookupResults = array();
13
    protected $fullTextResults = array();
14
    protected $highlight = array();
15
16
    /**
17
     * Search constructor.
18
     *
19
     * @param array  $pageLookupResults
20
     * @param array  $fullTextResults
21
     * @param string $highlight
22
     */
23
    public function __construct(array $pageLookupResults, array $fullTextResults, $highlight)
24
    {
25
        global $QUERY;
26
        $Indexer = idx_get_indexer();
27
28
        $this->query = $QUERY;
29
        $this->parsedQuery = ft_queryParser($Indexer, $QUERY);
30
        $this->searchState = new SearchState($this->parsedQuery);
31
32
        $this->pageLookupResults = $pageLookupResults;
33
        $this->fullTextResults = $fullTextResults;
34
        $this->highlight = $highlight;
0 ignored issues
show
Documentation Bug introduced by
It seems like $highlight of type string is incompatible with the declared type array of property $highlight.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
35
    }
36
37
    /**
38
     * display the search result
39
     *
40
     * @return void
41
     */
42
    public function show()
43
    {
44
        $searchHTML = '';
45
46
        $searchHTML .= $this->getSearchFormHTML($this->query);
47
48
        $searchHTML .= $this->getSearchIntroHTML($this->query);
49
50
        $searchHTML .= $this->getPageLookupHTML($this->pageLookupResults);
51
52
        $searchHTML .= $this->getFulltextResultsHTML($this->fullTextResults, $this->highlight);
53
54
        echo $searchHTML;
55
    }
56
57
    /**
58
     * Get a form which can be used to adjust/refine the search
59
     *
60
     * @param string $query
61
     *
62
     * @return string
63
     */
64
    protected function getSearchFormHTML($query)
65
    {
66
        global $lang, $ID, $INPUT;
67
68
        $searchForm = (new Form())->attrs(['method' => 'get'])->addClass('search-results-form');
69
        $searchForm->setHiddenField('do', 'search');
70
        $searchForm->setHiddenField('id', $ID);
71
        $searchForm->setHiddenField('searchPageForm', '1');
72
        if ($INPUT->has('after')) {
73
            $searchForm->setHiddenField('after', $INPUT->str('after'));
74
        }
75
        if ($INPUT->has('before')) {
76
            $searchForm->setHiddenField('before', $INPUT->str('before'));
77
        }
78
        if ($INPUT->has('sort')) {
79
            $searchForm->setHiddenField('sort', $INPUT->str('sort'));
80
        }
81
        $searchForm->addFieldsetOpen()->addClass('search-results-form__fieldset');
82
        $searchForm->addTextInput('q')->val($query)->useInput(false);
83
        $searchForm->addButton('', $lang['btn_search'])->attr('type', 'submit');
84
85
        if ($this->isSearchAssistanceAvailable($this->parsedQuery)) {
86
            $this->addSearchAssistanceElements($searchForm);
87
        } else {
88
            $searchForm->addClass('search-results-form--no-assistance');
89
            $searchForm->addTagOpen('span')->addClass('search-results-form__no-assistance-message');
90
            $searchForm->addHTML('FIXME Your query is too complex. Search assistance is unavailable. See <a href="https://doku.wiki/search">doku.wiki/search</a> for more help.');
91
            $searchForm->addTagClose('span');
92
        }
93
94
        $searchForm->addFieldsetClose();
95
96
        trigger_event('SEARCH_FORM_DISPLAY', $searchForm);
97
98
        return $searchForm->toHTML();
99
    }
100
101
    protected function addSortTool(Form $searchForm)
102
    {
103
        global $INPUT, $lang;
104
105
        $options = [
106
            'hits' => [
107
                'label' => $lang['search_sort_by_hits'],
108
                'sort' => '',
109
            ],
110
            'mtime' => [
111
                'label' => $lang['search_sort_by_mtime'],
112
                'sort' => 'mtime',
113
            ],
114
        ];
115
        $activeOption = 'hits';
116
117
        if ($INPUT->str('sort') === 'mtime') {
118
            $activeOption = 'mtime';
119
        }
120
121
        $searchForm->addTagOpen('div')->addClass('search-tool js-search-tool');
122
        // render current
123
        $currentWrapper = $searchForm->addTagOpen('div')->addClass('search-tool__current js-current');
124
        if ($activeOption !== 'hits') {
125
            $currentWrapper->addClass('search-tool__current--changed');
126
        }
127
        $searchForm->addHTML($options[$activeOption]['label']);
128
        $searchForm->addTagClose('div');
129
130
        // render options list
131
        $searchForm->addTagOpen('ul')->addClass('search-tool__options-list js-optionsList');
132
133
        foreach ($options as $key => $option) {
134
            $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
135
136
            if ($key === $activeOption) {
137
                $listItem->addClass('search-tool__options-list-item--active');
138
                $searchForm->addHTML($option['label']);
139
            } else {
140
                $this->searchState->addSearchLinkSort(
141
                    $searchForm,
142
                    $option['label'],
143
                    $option['sort']
144
                );
145
            }
146
            $searchForm->addTagClose('li');
147
        }
148
        $searchForm->addTagClose('ul');
149
150
        $searchForm->addTagClose('div');
151
152
    }
153
154
    /**
155
     * Decide if the given query is simple enough to provide search assistance
156
     *
157
     * @param array $parsedQuery
158
     *
159
     * @return bool
160
     */
161
    protected function isSearchAssistanceAvailable(array $parsedQuery)
162
    {
163
        if (count($parsedQuery['words']) > 1) {
164
            return false;
165
        }
166
        if (!empty($parsedQuery['not'])) {
167
            return false;
168
        }
169
170
        if (!empty($parsedQuery['phrases'])) {
171
            return false;
172
        }
173
174
        if (!empty($parsedQuery['notns'])) {
175
            return false;
176
        }
177
        if (count($parsedQuery['ns']) > 1) {
178
            return false;
179
        }
180
181
        return true;
182
    }
183
184
    /**
185
     * Add the elements to be used for search assistance
186
     *
187
     * @param Form $searchForm
188
     */
189
    protected function addSearchAssistanceElements(Form $searchForm)
190
    {
191
        $searchForm->addButton('toggleAssistant', 'toggle search assistant')
192
            ->attr('type', 'button')
193
            ->id('search-results-form__show-assistance-button')
194
            ->addClass('search-results-form__show-assistance-button');
195
196
        $searchForm->addTagOpen('div')
197
            ->addClass('js-advancedSearchOptions')
198
            ->attr('style', 'display: none;');
199
200
        $this->addFragmentBehaviorLinks($searchForm);
201
        $this->addNamespaceSelector($searchForm);
202
        $this->addDateSelector($searchForm);
203
        $this->addSortTool($searchForm);
204
205
        $searchForm->addTagClose('div');
206
    }
207
208
    protected function addFragmentBehaviorLinks(Form $searchForm)
209
    {
210
        global $lang;
211
212
        $options = [
213
            'exact' => [
214
                'label' => $lang['search_exact_match'],
215
                'and' => array_map(function ($term) {
216
                    return trim($term, '*');
217
                }, $this->parsedQuery['and']),
218
            ],
219
            'starts' => [
220
                'label' => $lang['search_starts_with'],
221
                'and' => array_map(function ($term) {
222
                    return trim($term, '*') . '*';
223
                }, $this->parsedQuery['and'])
224
            ],
225
            'ends' => [
226
                'label' => $lang['search_ends_with'],
227
                'and' => array_map(function ($term) {
228
                    return '*' . trim($term, '*');
229
                }, $this->parsedQuery['and'])
230
            ],
231
            'contains' => [
232
                'label' => $lang['search_contains'],
233
                'and' => array_map(function ($term) {
234
                    return '*' . trim($term, '*') . '*';
235
                }, $this->parsedQuery['and'])
236
            ]
237
        ];
238
239
        // detect current
240
        $activeOption = 'exact';
241
        foreach ($options as $key => $option) {
242
            if ($this->parsedQuery['and'] === $option['and']) {
243
                $activeOption = $key;
244
            }
245
        }
246
247
        $searchForm->addTagOpen('div')->addClass('search-tool js-search-tool');
248
        // render current
249
        $currentWrapper = $searchForm->addTagOpen('div')->addClass('search-tool__current js-current');
250
        if ($activeOption !== 'exact') {
251
            $currentWrapper->addClass('search-tool__current--changed');
252
        }
253
        $searchForm->addHTML($options[$activeOption]['label']);
254
        $searchForm->addTagClose('div');
255
256
        // render options list
257
        $searchForm->addTagOpen('ul')->addClass('search-tool__options-list js-optionsList');
258
259
        foreach ($options as $key => $option) {
260
            $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
261
262
            if ($key === $activeOption) {
263
                $listItem->addClass('search-tool__options-list-item--active');
264
                $searchForm->addHTML($option['label']);
265
            } else {
266
                $this->searchState->addSearchLinkFragment(
267
                    $searchForm,
268
                    $option['label'],
269
                    $option['and']
270
                );
271
            }
272
            $searchForm->addTagClose('li');
273
        }
274
        $searchForm->addTagClose('ul');
275
276
        $searchForm->addTagClose('div');
277
278
        // render options list
279
    }
280
281
    /**
282
     * Add the elements for the namespace selector
283
     *
284
     * @param Form $searchForm
285
     */
286
    protected function addNamespaceSelector(Form $searchForm)
287
    {
288
        global $lang;
289
290
        $baseNS = empty($this->parsedQuery['ns']) ? '' : $this->parsedQuery['ns'][0];
291
        $extraNS = $this->getAdditionalNamespacesFromResults($baseNS);
292
293
        $searchForm->addTagOpen('div')->addClass('search-tool js-search-tool');
294
        // render current
295
        $currentWrapper = $searchForm->addTagOpen('div')->addClass('search-tool__current js-current');
296
        if ($baseNS) {
297
            $currentWrapper->addClass('search-tool__current--changed');
298
            $searchForm->addHTML('@' . $baseNS);
299
        } else {
300
            $searchForm->addHTML($lang['search_any_ns']);
301
        }
302
        $searchForm->addTagClose('div');
303
304
        // render options list
305
        $searchForm->addTagOpen('ul')->addClass('search-tool__options-list js-optionsList');
306
307
        $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
308
        if ($baseNS) {
309
            $listItem->addClass('search-tool__options-list-item--active');
310
            $this->searchState->addSeachLinkNS(
311
                $searchForm,
312
                $lang['search_any_ns'],
313
                ''
314
            );
315
        } else {
316
            $searchForm->addHTML($lang['search_any_ns']);
317
        }
318
        $searchForm->addTagClose('li');
319
320
        foreach ($extraNS as $ns => $count) {
321
            $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
322
            $label = $ns . ($count ? " ($count)" : '');
323
324
            if ($ns === $baseNS) {
325
                $listItem->addClass('search-tool__options-list-item--active');
326
                $searchForm->addHTML($label);
327
            } else {
328
                $this->searchState->addSeachLinkNS(
329
                    $searchForm,
330
                    $label,
331
                    $ns
332
                );
333
            }
334
            $searchForm->addTagClose('li');
335
        }
336
        $searchForm->addTagClose('ul');
337
338
        $searchForm->addTagClose('div');
339
340
    }
341
342
    /**
343
     * Parse the full text results for their top namespaces below the given base namespace
344
     *
345
     * @param string $baseNS the namespace within which was searched, empty string for root namespace
346
     *
347
     * @return array an associative array with namespace => #number of found pages, sorted descending
348
     */
349
    protected function getAdditionalNamespacesFromResults($baseNS)
350
    {
351
        $namespaces = [];
352
        $baseNSLength = strlen($baseNS);
353
        foreach ($this->fullTextResults as $page => $numberOfHits) {
354
            $namespace = getNS($page);
355
            if (!$namespace) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $namespace of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
356
                continue;
357
            }
358
            if ($namespace === $baseNS) {
359
                continue;
360
            }
361
            $firstColon = strpos((string)$namespace, ':', $baseNSLength + 1) ?: strlen($namespace);
362
            $subtopNS = substr($namespace, 0, $firstColon);
363
            if (empty($namespaces[$subtopNS])) {
364
                $namespaces[$subtopNS] = 0;
365
            }
366
            $namespaces[$subtopNS] += 1;
367
        }
368
        arsort($namespaces);
369
        return $namespaces;
370
    }
371
372
    /**
373
     * @ToDo: custom date input
374
     *
375
     * @param Form $searchForm
376
     */
377
    protected function addDateSelector(Form $searchForm)
378
    {
379
        global $INPUT, $lang;
380
381
        $options = [
382
            'any' => [
383
                'before' => false,
384
                'after' => false,
385
                'label' => $lang['search_any_time'],
386
            ],
387
            'week' => [
388
                'before' => false,
389
                'after' => '1 week ago',
390
                'label' => $lang['search_past_7_days'],
391
            ],
392
            'month' => [
393
                'before' => false,
394
                'after' => '1 month ago',
395
                'label' => $lang['search_past_month'],
396
            ],
397
            'year' => [
398
                'before' => false,
399
                'after' => '1 year ago',
400
                'label' => $lang['search_past_year'],
401
            ],
402
        ];
403
        $activeOption = 'any';
404
        foreach ($options as $key => $option) {
405
            if ($INPUT->str('after') === $option['after']) {
406
                $activeOption = $key;
407
                break;
408
            }
409
        }
410
411
        $searchForm->addTagOpen('div')->addClass('search-tool js-search-tool');
412
        // render current
413
        $currentWrapper = $searchForm->addTagOpen('div')->addClass('search-tool__current js-current');
414
        if ($INPUT->has('before') || $INPUT->has('after')) {
415
            $currentWrapper->addClass('search-tool__current--changed');
416
        }
417
        $searchForm->addHTML($options[$activeOption]['label']);
418
        $searchForm->addTagClose('div');
419
420
        // render options list
421
        $searchForm->addTagOpen('ul')->addClass('search-tool__options-list js-optionsList');
422
423
        foreach ($options as $key => $option) {
424
            $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
425
426
            if ($key === $activeOption) {
427
                $listItem->addClass('search-tool__options-list-item--active');
428
                $searchForm->addHTML($option['label']);
429
            } else {
430
                $this->searchState->addSearchLinkTime(
431
                    $searchForm,
432
                    $option['label'],
433
                    $option['after'],
434
                    $option['before']
435
                );
436
            }
437
            $searchForm->addTagClose('li');
438
        }
439
        $searchForm->addTagClose('ul');
440
441
        $searchForm->addTagClose('div');
442
    }
443
444
445
    /**
446
     * Build the intro text for the search page
447
     *
448
     * @param string $query the search query
449
     *
450
     * @return string
451
     */
452
    protected function getSearchIntroHTML($query)
453
    {
454
        global $ID, $lang;
455
456
        $intro = p_locale_xhtml('searchpage');
457
        // allow use of placeholder in search intro
458
        $pagecreateinfo = (auth_quickaclcheck($ID) >= AUTH_CREATE) ? $lang['searchcreatepage'] : '';
459
        $intro = str_replace(
460
            array('@QUERY@', '@SEARCH@', '@CREATEPAGEINFO@'),
461
            array(hsc(rawurlencode($query)), hsc($query), $pagecreateinfo),
462
            $intro
463
        );
464
        return $intro;
465
    }
466
467
    /**
468
     * Build HTML for a list of pages with matching pagenames
469
     *
470
     * @param array $data search results
471
     *
472
     * @return string
473
     */
474
    protected function getPageLookupHTML($data)
475
    {
476
        if (empty($data)) {
477
            return '';
478
        }
479
480
        global $lang;
481
482
        $html = '<div class="search_quickresult">';
483
        $html .= '<h3>' . $lang['quickhits'] . ':</h3>';
484
        $html .= '<ul class="search_quickhits">';
485
        foreach ($data as $id => $title) {
486
            $link = html_wikilink(':' . $id);
487
            $eventData = [
488
                'listItemContent' => [$link],
489
                'page' => $id,
490
            ];
491
            trigger_event('SEARCH_RESULT_PAGELOOKUP', $eventData);
492
            $html .= '<li>' . implode('', $eventData['listItemContent']) . '</li>';
493
        }
494
        $html .= '</ul> ';
495
        //clear float (see http://www.complexspiral.com/publications/containing-floats/)
496
        $html .= '<div class="clearer"></div>';
497
        $html .= '</div>';
498
499
        return $html;
500
    }
501
502
    /**
503
     * Build HTML for fulltext search results or "no results" message
504
     *
505
     * @param array $data      the results of the fulltext search
506
     * @param array $highlight the terms to be highlighted in the results
507
     *
508
     * @return string
509
     */
510
    protected function getFulltextResultsHTML($data, $highlight)
511
    {
512
        global $lang;
513
514
        if (empty($data)) {
515
            return '<div class="nothing">' . $lang['nothingfound'] . '</div>';
516
        }
517
518
        $html = '';
519
        $html .= '<dl class="search_results">';
520
        $num = 1;
521
522
        foreach ($data as $id => $cnt) {
523
            $resultLink = html_wikilink(':' . $id, null, $highlight);
524
525
            $resultHeader = [$resultLink];
526
527
528
            $restrictQueryToNSLink = $this->restrictQueryToNSLink(getNS($id));
529
            if ($restrictQueryToNSLink) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $restrictQueryToNSLink of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
530
                $resultHeader[] = $restrictQueryToNSLink;
531
            }
532
533
            $snippet = '';
534
            $lastMod = '';
535
            $mtime = filemtime(wikiFN($id));
536
            if ($cnt !== 0) {
537
                $resultHeader[] = $cnt . ' ' . $lang['hits'];
538
                if ($num < FT_SNIPPET_NUMBER) { // create snippets for the first number of matches only
539
                    $snippet = '<dd>' . ft_snippet($id, $highlight) . '</dd>';
540
                    $lastMod = '<span class="search_results__lastmod">' . $lang['lastmod'] . ' ';
541
                    $lastMod .= '<time datetime="' . date_iso8601($mtime) . '">' . dformat($mtime) . '</time>';
542
                    $lastMod .= '</span>';
543
                }
544
                $num++;
545
            }
546
547
            $metaLine = '<div class="search_results__metaLine">';
548
            $metaLine .= $lastMod;
549
            $metaLine .= '</div>';
550
551
552
            $eventData = [
553
                'resultHeader' => $resultHeader,
554
                'resultBody' => [$metaLine, $snippet],
555
                'page' => $id,
556
            ];
557
            trigger_event('SEARCH_RESULT_FULLPAGE', $eventData);
558
            $html .= '<div class="search_fullpage_result">';
559
            $html .= '<dt>' . implode(' ', $eventData['resultHeader']) . '</dt>';
560
            $html .= implode('', $eventData['resultBody']);
561
            $html .= '</div>';
562
        }
563
        $html .= '</dl>';
564
565
        return $html;
566
    }
567
568
    /**
569
     * create a link to restrict the current query to a namespace
570
     *
571
     * @param bool|string $ns the namespace to which to restrict the query
572
     *
573
     * @return bool|string
574
     */
575
    protected function restrictQueryToNSLink($ns)
576
    {
577
        if (!$ns) {
578
            return false;
579
        }
580
        if (!$this->isSearchAssistanceAvailable($this->parsedQuery)) {
581
            return false;
582
        }
583
        if (!empty($this->parsedQuery['ns']) && $this->parsedQuery['ns'][0] === $ns) {
584
            return false;
585
        }
586
        $name = '@' . $ns;
587
        $tmpForm = new Form();
588
        $this->searchState->addSeachLinkNS($tmpForm, $name, $ns);
0 ignored issues
show
Bug introduced by
It seems like $ns defined by parameter $ns on line 575 can also be of type boolean; however, dokuwiki\Ui\SearchState::addSeachLinkNS() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
589
        return $tmpForm->toHTML();
590
    }
591
}
592