Completed
Push — betterCoreSearch ( 01c23b...c6b5b7 )
by Michael
08:12
created

Search   C

Complexity

Total Complexity 62

Size/Duplication

Total Lines 593
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 593
rs 5.9493
wmc 62
lcom 1
cbo 5

15 Methods

Rating   Name   Duplication   Size   Complexity  
A show() 0 14 1
A __construct() 0 13 1
B getSearchFormHTML() 0 29 4
B addSortTool() 0 52 5
A isNamespaceAssistanceAvailable() 0 7 2
A isFragmentAssistanceAvailable() 0 11 3
A addSearchAssistanceElements() 0 18 1
C addFragmentBehaviorLinks() 0 93 8
B addNamespaceSelector() 0 59 8
B getAdditionalNamespacesFromResults() 0 22 6
B addDateSelector() 0 66 7
A getSearchIntroHTML() 0 14 2
B getPageLookupHTML() 0 27 3
B getFulltextResultsHTML() 0 57 6
B restrictQueryToNSLink() 0 16 5

How to fix   Complexity   

Complex Class

Complex classes like Search often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Search, and based on these observations, apply Extract Interface, too.

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
        $this->addSearchAssistanceElements($searchForm);
86
87
        $searchForm->addFieldsetClose();
88
89
        trigger_event('SEARCH_FORM_DISPLAY', $searchForm);
90
91
        return $searchForm->toHTML();
92
    }
93
94
    protected function addSortTool(Form $searchForm)
95
    {
96
        global $INPUT, $lang;
97
98
        $options = [
99
            'hits' => [
100
                'label' => $lang['search_sort_by_hits'],
101
                'sort' => '',
102
            ],
103
            'mtime' => [
104
                'label' => $lang['search_sort_by_mtime'],
105
                'sort' => 'mtime',
106
            ],
107
        ];
108
        $activeOption = 'hits';
109
110
        if ($INPUT->str('sort') === 'mtime') {
111
            $activeOption = 'mtime';
112
        }
113
114
        $searchForm->addTagOpen('div')->addClass('search-tool js-search-tool');
115
        // render current
116
        $currentWrapper = $searchForm->addTagOpen('div')->addClass('search-tool__current js-current');
117
        if ($activeOption !== 'hits') {
118
            $currentWrapper->addClass('search-tool__current--changed');
119
        }
120
        $searchForm->addHTML($options[$activeOption]['label']);
121
        $searchForm->addTagClose('div');
122
123
        // render options list
124
        $searchForm->addTagOpen('ul')->addClass('search-tool__options-list js-optionsList');
125
126
        foreach ($options as $key => $option) {
127
            $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
128
129
            if ($key === $activeOption) {
130
                $listItem->addClass('search-tool__options-list-item--active');
131
                $searchForm->addHTML($option['label']);
132
            } else {
133
                $this->searchState->addSearchLinkSort(
134
                    $searchForm,
135
                    $option['label'],
136
                    $option['sort']
137
                );
138
            }
139
            $searchForm->addTagClose('li');
140
        }
141
        $searchForm->addTagClose('ul');
142
143
        $searchForm->addTagClose('div');
144
145
    }
146
147
    protected function isNamespaceAssistanceAvailable(array $parsedQuery) {
148
        if (preg_match('/[\(\)\|]/', $parsedQuery['query']) === 1) {
149
            return false;
150
        }
151
152
        return true;
153
    }
154
155
    protected function isFragmentAssistanceAvailable(array $parsedQuery) {
156
        if (preg_match('/[\(\)\|]/', $parsedQuery['query']) === 1) {
157
            return false;
158
        }
159
160
        if (!empty($parsedQuery['phrases'])) {
161
            return false;
162
        }
163
164
        return true;
165
    }
166
167
    /**
168
     * Add the elements to be used for search assistance
169
     *
170
     * @param Form $searchForm
171
     */
172
    protected function addSearchAssistanceElements(Form $searchForm)
173
    {
174
        $searchForm->addButton('toggleAssistant', 'toggle search assistant')
175
            ->attr('type', 'button')
176
            ->id('search-results-form__show-assistance-button')
177
            ->addClass('search-results-form__show-assistance-button');
178
179
        $searchForm->addTagOpen('div')
180
            ->addClass('js-advancedSearchOptions')
181
            ->attr('style', 'display: none;');
182
183
        $this->addFragmentBehaviorLinks($searchForm);
184
        $this->addNamespaceSelector($searchForm);
185
        $this->addDateSelector($searchForm);
186
        $this->addSortTool($searchForm);
187
188
        $searchForm->addTagClose('div');
189
    }
190
191
    protected function addFragmentBehaviorLinks(Form $searchForm)
192
    {
193
        if (!$this->isFragmentAssistanceAvailable($this->parsedQuery)) {
194
            return;
195
        }
196
        global $lang;
197
198
        $options = [
199
            'exact' => [
200
                'label' => $lang['search_exact_match'],
201
                'and' => array_map(function ($term) {
202
                    return trim($term, '*');
203
                }, $this->parsedQuery['and']),
204
                'not' => array_map(function ($term) {
205
                    return trim($term, '*');
206
                }, $this->parsedQuery['not']),
207
            ],
208
            'starts' => [
209
                'label' => $lang['search_starts_with'],
210
                'and' => array_map(function ($term) {
211
                    return trim($term, '*') . '*';
212
                }, $this->parsedQuery['and']),
213
                'not' => array_map(function ($term) {
214
                    return trim($term, '*') . '*';
215
                }, $this->parsedQuery['not']),
216
            ],
217
            'ends' => [
218
                'label' => $lang['search_ends_with'],
219
                'and' => array_map(function ($term) {
220
                    return '*' . trim($term, '*');
221
                }, $this->parsedQuery['and']),
222
                'not' => array_map(function ($term) {
223
                    return '*' . trim($term, '*');
224
                }, $this->parsedQuery['not']),
225
            ],
226
            'contains' => [
227
                'label' => $lang['search_contains'],
228
                'and' => array_map(function ($term) {
229
                    return '*' . trim($term, '*') . '*';
230
                }, $this->parsedQuery['and']),
231
                'not' => array_map(function ($term) {
232
                    return '*' . trim($term, '*') . '*';
233
                }, $this->parsedQuery['not']),
234
            ]
235
        ];
236
237
        // detect current
238
        $activeOption = 'custom';
239
        foreach ($options as $key => $option) {
240
            if ($this->parsedQuery['and'] === $option['and']) {
241
                $activeOption = $key;
242
            }
243
        }
244
        if ($activeOption === 'custom') {
245
            $options = array_merge(['custom' => [
246
                'label' => $lang['search_custom_match'],
247
            ]], $options);
248
        }
249
250
        $searchForm->addTagOpen('div')->addClass('search-tool js-search-tool');
251
        // render current
252
        $currentWrapper = $searchForm->addTagOpen('div')->addClass('search-tool__current js-current');
253
        if ($activeOption !== 'exact') {
254
            $currentWrapper->addClass('search-tool__current--changed');
255
        }
256
        $searchForm->addHTML($options[$activeOption]['label']);
257
        $searchForm->addTagClose('div');
258
259
        // render options list
260
        $searchForm->addTagOpen('ul')->addClass('search-tool__options-list js-optionsList');
261
262
        foreach ($options as $key => $option) {
263
            $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
264
265
            if ($key === $activeOption) {
266
                $listItem->addClass('search-tool__options-list-item--active');
267
                $searchForm->addHTML($option['label']);
268
            } else {
269
                $this->searchState->addSearchLinkFragment(
270
                    $searchForm,
271
                    $option['label'],
272
                    $option['and'],
273
                    $option['not']
274
                );
275
            }
276
            $searchForm->addTagClose('li');
277
        }
278
        $searchForm->addTagClose('ul');
279
280
        $searchForm->addTagClose('div');
281
282
        // render options list
283
    }
284
285
    /**
286
     * Add the elements for the namespace selector
287
     *
288
     * @param Form $searchForm
289
     */
290
    protected function addNamespaceSelector(Form $searchForm)
291
    {
292
        if (!$this->isNamespaceAssistanceAvailable($this->parsedQuery)) {
293
            return;
294
        }
295
296
        global $lang;
297
298
        $baseNS = empty($this->parsedQuery['ns']) ? '' : $this->parsedQuery['ns'][0];
299
        $extraNS = $this->getAdditionalNamespacesFromResults($baseNS);
300
301
        $searchForm->addTagOpen('div')->addClass('search-tool js-search-tool');
302
        // render current
303
        $currentWrapper = $searchForm->addTagOpen('div')->addClass('search-tool__current js-current');
304
        if ($baseNS) {
305
            $currentWrapper->addClass('search-tool__current--changed');
306
            $searchForm->addHTML('@' . $baseNS);
307
        } else {
308
            $searchForm->addHTML($lang['search_any_ns']);
309
        }
310
        $searchForm->addTagClose('div');
311
312
        // render options list
313
        $searchForm->addTagOpen('ul')->addClass('search-tool__options-list js-optionsList');
314
315
        $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
316
        if ($baseNS) {
317
            $listItem->addClass('search-tool__options-list-item--active');
318
            $this->searchState->addSeachLinkNS(
319
                $searchForm,
320
                $lang['search_any_ns'],
321
                ''
322
            );
323
        } else {
324
            $searchForm->addHTML($lang['search_any_ns']);
325
        }
326
        $searchForm->addTagClose('li');
327
328
        foreach ($extraNS as $ns => $count) {
329
            $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
330
            $label = $ns . ($count ? " ($count)" : '');
331
332
            if ($ns === $baseNS) {
333
                $listItem->addClass('search-tool__options-list-item--active');
334
                $searchForm->addHTML($label);
335
            } else {
336
                $this->searchState->addSeachLinkNS(
337
                    $searchForm,
338
                    $label,
339
                    $ns
340
                );
341
            }
342
            $searchForm->addTagClose('li');
343
        }
344
        $searchForm->addTagClose('ul');
345
346
        $searchForm->addTagClose('div');
347
348
    }
349
350
    /**
351
     * Parse the full text results for their top namespaces below the given base namespace
352
     *
353
     * @param string $baseNS the namespace within which was searched, empty string for root namespace
354
     *
355
     * @return array an associative array with namespace => #number of found pages, sorted descending
356
     */
357
    protected function getAdditionalNamespacesFromResults($baseNS)
358
    {
359
        $namespaces = [];
360
        $baseNSLength = strlen($baseNS);
361
        foreach ($this->fullTextResults as $page => $numberOfHits) {
362
            $namespace = getNS($page);
363
            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...
364
                continue;
365
            }
366
            if ($namespace === $baseNS) {
367
                continue;
368
            }
369
            $firstColon = strpos((string)$namespace, ':', $baseNSLength + 1) ?: strlen($namespace);
370
            $subtopNS = substr($namespace, 0, $firstColon);
371
            if (empty($namespaces[$subtopNS])) {
372
                $namespaces[$subtopNS] = 0;
373
            }
374
            $namespaces[$subtopNS] += 1;
375
        }
376
        arsort($namespaces);
377
        return $namespaces;
378
    }
379
380
    /**
381
     * @ToDo: custom date input
382
     *
383
     * @param Form $searchForm
384
     */
385
    protected function addDateSelector(Form $searchForm)
386
    {
387
        global $INPUT, $lang;
388
389
        $options = [
390
            'any' => [
391
                'before' => false,
392
                'after' => false,
393
                'label' => $lang['search_any_time'],
394
            ],
395
            'week' => [
396
                'before' => false,
397
                'after' => '1 week ago',
398
                'label' => $lang['search_past_7_days'],
399
            ],
400
            'month' => [
401
                'before' => false,
402
                'after' => '1 month ago',
403
                'label' => $lang['search_past_month'],
404
            ],
405
            'year' => [
406
                'before' => false,
407
                'after' => '1 year ago',
408
                'label' => $lang['search_past_year'],
409
            ],
410
        ];
411
        $activeOption = 'any';
412
        foreach ($options as $key => $option) {
413
            if ($INPUT->str('after') === $option['after']) {
414
                $activeOption = $key;
415
                break;
416
            }
417
        }
418
419
        $searchForm->addTagOpen('div')->addClass('search-tool js-search-tool');
420
        // render current
421
        $currentWrapper = $searchForm->addTagOpen('div')->addClass('search-tool__current js-current');
422
        if ($INPUT->has('before') || $INPUT->has('after')) {
423
            $currentWrapper->addClass('search-tool__current--changed');
424
        }
425
        $searchForm->addHTML($options[$activeOption]['label']);
426
        $searchForm->addTagClose('div');
427
428
        // render options list
429
        $searchForm->addTagOpen('ul')->addClass('search-tool__options-list js-optionsList');
430
431
        foreach ($options as $key => $option) {
432
            $listItem = $searchForm->addTagOpen('li')->addClass('search-tool__options-list-item');
433
434
            if ($key === $activeOption) {
435
                $listItem->addClass('search-tool__options-list-item--active');
436
                $searchForm->addHTML($option['label']);
437
            } else {
438
                $this->searchState->addSearchLinkTime(
439
                    $searchForm,
440
                    $option['label'],
441
                    $option['after'],
442
                    $option['before']
443
                );
444
            }
445
            $searchForm->addTagClose('li');
446
        }
447
        $searchForm->addTagClose('ul');
448
449
        $searchForm->addTagClose('div');
450
    }
451
452
453
    /**
454
     * Build the intro text for the search page
455
     *
456
     * @param string $query the search query
457
     *
458
     * @return string
459
     */
460
    protected function getSearchIntroHTML($query)
461
    {
462
        global $ID, $lang;
463
464
        $intro = p_locale_xhtml('searchpage');
465
        // allow use of placeholder in search intro
466
        $pagecreateinfo = (auth_quickaclcheck($ID) >= AUTH_CREATE) ? $lang['searchcreatepage'] : '';
467
        $intro = str_replace(
468
            array('@QUERY@', '@SEARCH@', '@CREATEPAGEINFO@'),
469
            array(hsc(rawurlencode($query)), hsc($query), $pagecreateinfo),
470
            $intro
471
        );
472
        return $intro;
473
    }
474
475
    /**
476
     * Build HTML for a list of pages with matching pagenames
477
     *
478
     * @param array $data search results
479
     *
480
     * @return string
481
     */
482
    protected function getPageLookupHTML($data)
483
    {
484
        if (empty($data)) {
485
            return '';
486
        }
487
488
        global $lang;
489
490
        $html = '<div class="search_quickresult">';
491
        $html .= '<h3>' . $lang['quickhits'] . ':</h3>';
492
        $html .= '<ul class="search_quickhits">';
493
        foreach ($data as $id => $title) {
494
            $link = html_wikilink(':' . $id);
495
            $eventData = [
496
                'listItemContent' => [$link],
497
                'page' => $id,
498
            ];
499
            trigger_event('SEARCH_RESULT_PAGELOOKUP', $eventData);
500
            $html .= '<li>' . implode('', $eventData['listItemContent']) . '</li>';
501
        }
502
        $html .= '</ul> ';
503
        //clear float (see http://www.complexspiral.com/publications/containing-floats/)
504
        $html .= '<div class="clearer"></div>';
505
        $html .= '</div>';
506
507
        return $html;
508
    }
509
510
    /**
511
     * Build HTML for fulltext search results or "no results" message
512
     *
513
     * @param array $data      the results of the fulltext search
514
     * @param array $highlight the terms to be highlighted in the results
515
     *
516
     * @return string
517
     */
518
    protected function getFulltextResultsHTML($data, $highlight)
519
    {
520
        global $lang;
521
522
        if (empty($data)) {
523
            return '<div class="nothing">' . $lang['nothingfound'] . '</div>';
524
        }
525
526
        $html = '';
527
        $html .= '<dl class="search_results">';
528
        $num = 1;
529
530
        foreach ($data as $id => $cnt) {
531
            $resultLink = html_wikilink(':' . $id, null, $highlight);
532
533
            $resultHeader = [$resultLink];
534
535
536
            $restrictQueryToNSLink = $this->restrictQueryToNSLink(getNS($id));
537
            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...
538
                $resultHeader[] = $restrictQueryToNSLink;
539
            }
540
541
            $snippet = '';
542
            $lastMod = '';
543
            $mtime = filemtime(wikiFN($id));
544
            if ($cnt !== 0) {
545
                $resultHeader[] = $cnt . ' ' . $lang['hits'];
546
                if ($num < FT_SNIPPET_NUMBER) { // create snippets for the first number of matches only
547
                    $snippet = '<dd>' . ft_snippet($id, $highlight) . '</dd>';
548
                    $lastMod = '<span class="search_results__lastmod">' . $lang['lastmod'] . ' ';
549
                    $lastMod .= '<time datetime="' . date_iso8601($mtime) . '">' . dformat($mtime) . '</time>';
550
                    $lastMod .= '</span>';
551
                }
552
                $num++;
553
            }
554
555
            $metaLine = '<div class="search_results__metaLine">';
556
            $metaLine .= $lastMod;
557
            $metaLine .= '</div>';
558
559
560
            $eventData = [
561
                'resultHeader' => $resultHeader,
562
                'resultBody' => [$metaLine, $snippet],
563
                'page' => $id,
564
            ];
565
            trigger_event('SEARCH_RESULT_FULLPAGE', $eventData);
566
            $html .= '<div class="search_fullpage_result">';
567
            $html .= '<dt>' . implode(' ', $eventData['resultHeader']) . '</dt>';
568
            $html .= implode('', $eventData['resultBody']);
569
            $html .= '</div>';
570
        }
571
        $html .= '</dl>';
572
573
        return $html;
574
    }
575
576
    /**
577
     * create a link to restrict the current query to a namespace
578
     *
579
     * @param bool|string $ns the namespace to which to restrict the query
580
     *
581
     * @return bool|string
582
     */
583
    protected function restrictQueryToNSLink($ns)
584
    {
585
        if (!$ns) {
586
            return false;
587
        }
588
        if (!$this->isNamespaceAssistanceAvailable($this->parsedQuery)) {
589
            return false;
590
        }
591
        if (!empty($this->parsedQuery['ns']) && $this->parsedQuery['ns'][0] === $ns) {
592
            return false;
593
        }
594
        $name = '@' . $ns;
595
        $tmpForm = new Form();
596
        $this->searchState->addSeachLinkNS($tmpForm, $name, $ns);
0 ignored issues
show
Bug introduced by
It seems like $ns defined by parameter $ns on line 583 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...
597
        return $tmpForm->toHTML();
598
    }
599
}
600