Completed
Push — betterCoreSearch ( de3383...bbc1da )
by Michael
04:19
created

Search::execute()   D

Complexity

Conditions 10
Paths 17

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 17
nc 17
nop 0
dl 0
loc 28
rs 4.8196
c 0
b 0
f 0

How to fix   Complexity   

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 $pageLookupResults = array();
12
    protected $fullTextResults = array();
13
    protected $highlight = array();
14
15
    /**
16
     * Search constructor.
17
     *
18
     * @param string $query the search query
0 ignored issues
show
Bug introduced by
There is no parameter named $query. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
19
     */
20
    public function __construct()
21
    {
22
        global $QUERY;
23
        $Indexer = idx_get_indexer();
24
25
        $this->query = $QUERY;
26
        $this->parsedQuery = ft_queryParser($Indexer, $QUERY);
27
    }
28
29
    /**
30
     * run the search
31
     */
32
    public function execute()
33
    {
34
        $this->pageLookupResults = ft_pageLookup($this->query, true, useHeading('navigation'));
35
        $this->fullTextResults = ft_pageSearch($this->query, $highlight);
36
        $this->highlight = $highlight;
37
38
        // fixme: find better place for this
39
        global $INPUT;
40
        if ($INPUT->has('after') || $INPUT->has('before')) {
41
            $after = $INPUT->str('after');
42
            $after = is_int($after) ? $after : strtotime($after);
43
44
            $before = $INPUT->str('before');
45
            $before = is_int($before) ? $before : strtotime($before);
46
47
            // todo: should we filter $this->pageLookupResults as well?
48
            foreach ($this->fullTextResults as $id => $cnt) {
49
                $mTime = filemtime(wikiFN($id));
50
                if ($after && $after > $mTime) {
51
                    unset($this->fullTextResults[$id]);
52
                    continue;
53
                }
54
                if ($before && $before < $mTime) {
55
                    unset($this->fullTextResults[$id]);
56
                }
57
            }
58
        }
59
    }
60
61
    /**
62
     * display the search result
63
     *
64
     * @return void
65
     */
66
    public function show()
67
    {
68
        $searchHTML = '';
69
70
        $searchHTML .= $this->getSearchFormHTML($this->query);
71
72
        $searchHTML .= $this->getSearchIntroHTML($this->query);
73
74
        $searchHTML .= $this->getPageLookupHTML($this->pageLookupResults);
75
76
        $searchHTML .= $this->getFulltextResultsHTML($this->fullTextResults, $this->highlight);
77
78
        echo $searchHTML;
79
    }
80
81
    /**
82
     * Get a form which can be used to adjust/refine the search
83
     *
84
     * @param string $query
85
     *
86
     * @return string
87
     */
88
    protected function getSearchFormHTML($query)
89
    {
90
        global $lang, $ID, $INPUT;
91
92
        $searchForm = (new Form())->attrs(['method' => 'get'])->addClass('search-results-form');
93
        $searchForm->setHiddenField('do', 'search');
94
        $searchForm->setHiddenField('from', $ID);
95
        $searchForm->setHiddenField('searchPageForm', '1');
96
        if ($INPUT->has('after')) {
97
            $searchForm->setHiddenField('after', $INPUT->str('after'));
98
        }
99
        if ($INPUT->has('before')) {
100
            $searchForm->setHiddenField('before', $INPUT->str('before'));
101
        }
102
        $searchForm->addFieldsetOpen()->addClass('search-results-form__fieldset');
103
        $searchForm->addTextInput('id')->val($query)->useInput(false);
104
        $searchForm->addButton('', $lang['btn_search'])->attr('type', 'submit');
105
106
        if ($this->isSearchAssistanceAvailable($this->parsedQuery)) {
107
            $this->addSearchAssistanceElements($searchForm, $this->parsedQuery);
108
        } else {
109
            $searchForm->addClass('search-results-form--no-assistance');
110
            $searchForm->addTagOpen('span')->addClass('search-results-form__no-assistance-message');
111
            $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.');
112
            $searchForm->addTagClose('span');
113
        }
114
115
        $searchForm->addFieldsetClose();
116
117
        trigger_event('SEARCH_FORM_DISPLAY', $searchForm);
118
119
        return $searchForm->toHTML();
120
    }
121
122
    /**
123
     * Decide if the given query is simple enough to provide search assistance
124
     *
125
     * @param array $parsedQuery
126
     *
127
     * @return bool
128
     */
129
    protected function isSearchAssistanceAvailable(array $parsedQuery)
130
    {
131
        if (count($parsedQuery['words']) > 1) {
132
            return false;
133
        }
134
        if (!empty($parsedQuery['not'])) {
135
            return false;
136
        }
137
138
        if (!empty($parsedQuery['phrases'])) {
139
            return false;
140
        }
141
142
        if (!empty($parsedQuery['notns'])) {
143
            return false;
144
        }
145
        if (count($parsedQuery['ns']) > 1) {
146
            return false;
147
        }
148
149
        return true;
150
    }
151
152
    /**
153
     * Add the elements to be used for search assistance
154
     *
155
     * @param Form  $searchForm
156
     * @param array $parsedQuery
157
     */
158
    protected function addSearchAssistanceElements(Form $searchForm, array $parsedQuery)
159
    {
160
        $searchForm->addButton('toggleAssistant', 'toggle search assistant')
161
            ->attr('type', 'button')
162
            ->id('search-results-form__show-assistance-button')
163
            ->addClass('search-results-form__show-assistance-button');
164
165
        $searchForm->addTagOpen('div')
166
            ->addClass('js-advancedSearchOptions')
167
            ->attr('style', 'display: none;');
168
169
        $this->addFragmentBehaviorLinks($searchForm, $parsedQuery);
170
        $this->addNamespaceSelector($searchForm, $parsedQuery);
171
        $this->addDateSelector($searchForm, $parsedQuery);
172
173
        $searchForm->addTagClose('div');
174
    }
175
176
    protected function addFragmentBehaviorLinks(Form $searchForm, array $parsedQuery)
0 ignored issues
show
Unused Code introduced by
The parameter $parsedQuery is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
177
    {
178
        $searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
179
        $searchForm->addHTML('fragment behavior: ');
180
181
        $this->addSearchLink(
182
            $searchForm,
183
            'exact match',
184
            array_map(function($term){return trim($term, '*');},$this->parsedQuery['and'])
185
        );
186
187
        $searchForm->addHTML(', ');
188
189
        $this->addSearchLink(
190
            $searchForm,
191
            'starts with',
192
            array_map(function($term){return trim($term, '*') . '*';},$this->parsedQuery['and'])
193
        );
194
195
        $searchForm->addHTML(', ');
196
197
        $this->addSearchLink(
198
            $searchForm,
199
            'ends with',
200
            array_map(function($term){return '*' . trim($term, '*');},$this->parsedQuery['and'])
201
        );
202
203
        $searchForm->addHTML(', ');
204
205
        $this->addSearchLink(
206
            $searchForm,
207
            'contains',
208
            array_map(function($term){return '*' . trim($term, '*') . '*';},$this->parsedQuery['and'])
209
        );
210
211
        $searchForm->addTagClose('div');
212
    }
213
214
    protected function addSearchLink(
215
        Form $searchForm,
216
        $label,
217
        array $and = null,
218
        array $ns = null,
219
        array $not = null,
220
        array $notns = null,
221
        array $phrases = null,
222
        $after = null,
223
        $before = null
224
    ) {
225
        global $INPUT;
226
        if (null === $and) {
227
            $and = $this->parsedQuery['and'];
228
        }
229
        if (null === $ns) {
230
            $ns = $this->parsedQuery['ns'];
231
        }
232
        if (null === $not) {
233
            $not = $this->parsedQuery['not'];
234
        }
235
        if (null === $phrases) {
236
            $phrases = $this->parsedQuery['phrases'];
237
        }
238
        if (null === $notns) {
239
            $notns = $this->parsedQuery['notns'];
240
        }
241
        if (null === $after) {
242
            $after = $INPUT->str('after');
243
        }
244
        if (null === $before) {
245
            $before = $INPUT->str('before');
246
        }
247
248
        $newQuery = ft_queryUnparser_simple(
249
            $and,
0 ignored issues
show
Bug introduced by
It seems like $and can also be of type string; however, ft_queryUnparser_simple() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
250
            $not,
0 ignored issues
show
Bug introduced by
It seems like $not can also be of type string; however, ft_queryUnparser_simple() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
251
            $phrases,
0 ignored issues
show
Bug introduced by
It seems like $phrases can also be of type string; however, ft_queryUnparser_simple() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
252
            $ns,
0 ignored issues
show
Bug introduced by
It seems like $ns can also be of type string; however, ft_queryUnparser_simple() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
253
            $notns
0 ignored issues
show
Bug introduced by
It seems like $notns can also be of type string; however, ft_queryUnparser_simple() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
254
        );
255
        $hrefAttributes = ['do' => 'search', 'searchPageForm' => '1'];
256
        if ($after) {
257
            $hrefAttributes['after'] = $after;
258
        }
259
        if ($before) {
260
            $hrefAttributes['before'] = $before;
261
        }
262
        $searchForm->addTagOpen('a')
263
            ->attrs([
264
                'href' => wl($newQuery, $hrefAttributes, false, '&')
265
            ])
266
        ;
267
        $searchForm->addHTML($label);
268
        $searchForm->addTagClose('a');
269
    }
270
271
    /**
272
     * Add the elements for the namespace selector
273
     *
274
     * @param Form  $searchForm
275
     * @param array $parsedQuery
276
     */
277
    protected function addNamespaceSelector(Form $searchForm, array $parsedQuery)
278
    {
279
        $baseNS = empty($parsedQuery['ns']) ? '' : $parsedQuery['ns'][0];
280
        $searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
281
282
        $extraNS = $this->getAdditionalNamespacesFromResults($baseNS);
283
        if (!empty($extraNS) || $baseNS) {
284
            $searchForm->addTagOpen('div');
285
            $searchForm->addHTML('limit to namespace: ');
286
287
            if ($baseNS) {
288
                $this->addSearchLink(
289
                    $searchForm,
290
                    '(remove limit)',
291
                    null,
292
                    [],
293
                    null,
294
                    []
295
                );
296
            }
297
298
            foreach ($extraNS as $extraNS => $count) {
0 ignored issues
show
Bug introduced by
The expression $extraNS of type integer|string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
299
                $searchForm->addHTML(' ');
300
                $label = $extraNS . ($count ? " ($count)" : '');
301
302
                $this->addSearchLink($searchForm, $label, null, [$extraNS], null, []);
303
            }
304
            $searchForm->addTagClose('div');
305
        }
306
307
        $searchForm->addTagClose('div');
308
    }
309
310
    /**
311
     * Parse the full text results for their top namespaces below the given base namespace
312
     *
313
     * @param string $baseNS the namespace within which was searched, empty string for root namespace
314
     *
315
     * @return array an associative array with namespace => #number of found pages, sorted descending
316
     */
317
    protected function getAdditionalNamespacesFromResults($baseNS)
318
    {
319
        $namespaces = [];
320
        $baseNSLength = strlen($baseNS);
321
        foreach ($this->fullTextResults as $page => $numberOfHits) {
322
            $namespace = getNS($page);
323
            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...
324
                continue;
325
            }
326
            if ($namespace === $baseNS) {
327
                continue;
328
            }
329
            $firstColon = strpos((string)$namespace, ':', $baseNSLength + 1) ?: strlen($namespace);
330
            $subtopNS = substr($namespace, 0, $firstColon);
331
            if (empty($namespaces[$subtopNS])) {
332
                $namespaces[$subtopNS] = 0;
333
            }
334
            $namespaces[$subtopNS] += 1;
335
        }
336
        arsort($namespaces);
337
        return $namespaces;
338
    }
339
340
    /**
341
     * @ToDo: we need to remember this date when clicking on other links
342
     * @ToDo: custom date input
343
     *
344
     * @param Form $searchForm
345
     * @param      $parsedQuery
346
     */
347
    protected function addDateSelector(Form $searchForm, $parsedQuery) {
0 ignored issues
show
Unused Code introduced by
The parameter $parsedQuery is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
348
        $searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
349
        $searchForm->addHTML('limit by date: ');
350
351
        global $INPUT;
352
        if ($INPUT->has('before') || $INPUT->has('after')) {
353
            $this->addSearchLink(
354
                $searchForm,
355
                '(remove limit)',
356
                null,
357
                null,
358
                null,
359
                null,
360
                null,
361
                false,
362
                false
363
            );
364
365
            $searchForm->addHTML(', ');
366
        }
367
368
        if ($INPUT->str('after') === '1 week ago') {
369
            $searchForm->addHTML('<span class="active">past 7 days</span>');
370
        } else {
371
            $this->addSearchLink(
372
                $searchForm,
373
                'past 7 days',
374
                null,
375
                null,
376
                null,
377
                null,
378
                null,
379
                '1 week ago',
380
                false
381
            );
382
        }
383
384
        $searchForm->addHTML(', ');
385
386
        if ($INPUT->str('after') === '1 month ago') {
387
            $searchForm->addHTML('<span class="active">past month</span>');
388
        } else {
389
            $this->addSearchLink(
390
                $searchForm,
391
                'past month',
392
                null,
393
                null,
394
                null,
395
                null,
396
                null,
397
                '1 month ago',
398
                false
399
            );
400
        }
401
402
        $searchForm->addHTML(', ');
403
404
        if ($INPUT->str('after') === '1 year ago') {
405
            $searchForm->addHTML('<span class="active">past year</span>');
406
        } else {
407
            $this->addSearchLink(
408
                $searchForm,
409
                'past year',
410
                null,
411
                null,
412
                null,
413
                null,
414
                null,
415
                '1 year ago',
416
                false
417
            );
418
        }
419
420
        $searchForm->addTagClose('div');
421
    }
422
423
424
    /**
425
     * Build the intro text for the search page
426
     *
427
     * @param string $query the search query
428
     *
429
     * @return string
430
     */
431
    protected function getSearchIntroHTML($query)
432
    {
433
        global $ID, $lang;
434
435
        $intro = p_locale_xhtml('searchpage');
436
        // allow use of placeholder in search intro
437
        $pagecreateinfo = (auth_quickaclcheck($ID) >= AUTH_CREATE) ? $lang['searchcreatepage'] : '';
438
        $intro = str_replace(
439
            array('@QUERY@', '@SEARCH@', '@CREATEPAGEINFO@'),
440
            array(hsc(rawurlencode($query)), hsc($query), $pagecreateinfo),
441
            $intro
442
        );
443
        return $intro;
444
    }
445
446
    /**
447
     * Build HTML for a list of pages with matching pagenames
448
     *
449
     * @param array $data search results
450
     *
451
     * @return string
452
     */
453
    protected function getPageLookupHTML($data)
454
    {
455
        if (empty($data)) {
456
            return '';
457
        }
458
459
        global $lang;
460
461
        $html = '<div class="search_quickresult">';
462
        $html .= '<h3>' . $lang['quickhits'] . ':</h3>';
463
        $html .= '<ul class="search_quickhits">';
464
        foreach ($data as $id => $title) {
465
            $link = html_wikilink(':' . $id);
466
            $eventData = [
467
                'listItemContent' => [$link],
468
                'page' => $id,
469
            ];
470
            trigger_event('SEARCH_RESULT_PAGELOOKUP', $eventData);
471
            $html .= '<li>' . implode('', $eventData['listItemContent']) . '</li>';
472
        }
473
        $html .= '</ul> ';
474
        //clear float (see http://www.complexspiral.com/publications/containing-floats/)
475
        $html .= '<div class="clearer"></div>';
476
        $html .= '</div>';
477
478
        return $html;
479
    }
480
481
    /**
482
     * Build HTML for fulltext search results or "no results" message
483
     *
484
     * @param array $data      the results of the fulltext search
485
     * @param array $highlight the terms to be highlighted in the results
486
     *
487
     * @return string
488
     */
489
    protected function getFulltextResultsHTML($data, $highlight)
490
    {
491
        global $lang;
492
493
        if (empty($data)) {
494
            return '<div class="nothing">' . $lang['nothingfound'] . '</div>';
495
        }
496
497
        $html = '';
498
        $html .= '<dl class="search_results">';
499
        $num = 1;
500
501
        foreach ($data as $id => $cnt) {
502
            $resultLink = html_wikilink(':' . $id, null, $highlight);
503
504
            $resultHeader = [$resultLink];
505
506
507
            $restrictQueryToNSLink = $this->restrictQueryToNSLink(getNS($id));
508
            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...
509
                $resultHeader[] = $restrictQueryToNSLink;
510
            }
511
512
            $snippet = '';
513
            $lastMod = '';
514
            $mtime = filemtime(wikiFN($id));
515
            if ($cnt !== 0) {
516
                $resultHeader[] = $cnt . ' ' . $lang['hits'];
517
                if ($num < FT_SNIPPET_NUMBER) { // create snippets for the first number of matches only
518
                    $snippet = '<dd>' . ft_snippet($id, $highlight) . '</dd>';
519
                    $lastMod = '<span class="search_results__lastmod">'. $lang['lastmod'] . ' ';
520
                    $lastMod .= '<time datetime="' . date_iso8601($mtime) . '">'. dformat($mtime) . '</time>';
521
                    $lastMod .= '</span>';
522
                }
523
                $num++;
524
            }
525
526
            $metaLine = '<div class="search_results__metaLine">';
527
            $metaLine .= $lastMod;
528
            $metaLine .= '</div>';
529
530
531
            $eventData = [
532
                'resultHeader' => $resultHeader,
533
                'resultBody' => [$metaLine, $snippet],
534
                'page' => $id,
535
            ];
536
            trigger_event('SEARCH_RESULT_FULLPAGE', $eventData);
537
            $html .= '<div class="search_fullpage_result">';
538
            $html .= '<dt>' . implode(' ', $eventData['resultHeader']) . '</dt>';
539
            $html .= implode('', $eventData['resultBody']);
540
            $html .= '</div>';
541
        }
542
        $html .= '</dl>';
543
544
        return $html;
545
    }
546
547
    /**
548
     * create a link to restrict the current query to a namespace
549
     *
550
     * @param bool|string $ns the namespace to which to restrict the query
551
     *
552
     * @return bool|string
553
     */
554
    protected function restrictQueryToNSLink($ns)
555
    {
556
        if (!$ns) {
557
            return false;
558
        }
559
        if (!$this->isSearchAssistanceAvailable($this->parsedQuery)) {
560
            return false;
561
        }
562
        if (!empty($this->parsedQuery['ns']) && $this->parsedQuery['ns'][0] === $ns) {
563
            return false;
564
        }
565
        $name = '@' . $ns;
566
        $tmpForm = new Form();
567
        $this->addSearchLink($tmpForm, $name, null, [$ns], null, []);
568
        return $tmpForm->toHTML();
569
    }
570
}
571