Completed
Push — betterCoreSearch ( d22b78...b3cfe8 )
by Michael
09:15 queued 05:11
created

Search::filterResultsByTime()   C

Complexity

Conditions 10
Paths 5

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 15
nc 5
nop 1
dl 0
loc 24
rs 5.2164
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 = $this->filterResultsByTime(
35
            ft_pageLookup($this->query, true, useHeading('navigation'))
36
        );
37
        $this->fullTextResults = $this->filterResultsByTime(
38
            ft_pageSearch($this->query, $highlight)
39
        );
40
        $this->highlight = $highlight;
41
    }
42
43
    /**
44
     * @param array $results search results in the form pageid => value
45
     *
46
     * @return array
47
     */
48
    protected function filterResultsByTime(array $results) {
49
        global $INPUT;
50
        if ($INPUT->has('after') || $INPUT->has('before')) {
51
            $after = $INPUT->str('after');
52
            $after = is_int($after) ? $after : strtotime($after);
53
54
            $before = $INPUT->str('before');
55
            $before = is_int($before) ? $before : strtotime($before);
56
57
            // todo: should we filter $this->pageLookupResults as well?
58
            foreach ($results as $id => $value) {
59
                $mTime = filemtime(wikiFN($id));
60
                if ($after && $after > $mTime) {
61
                    unset($results[$id]);
62
                    continue;
63
                }
64
                if ($before && $before < $mTime) {
65
                    unset($results[$id]);
66
                }
67
            }
68
        }
69
70
        return $results;
71
    }
72
73
    /**
74
     * display the search result
75
     *
76
     * @return void
77
     */
78
    public function show()
79
    {
80
        $searchHTML = '';
81
82
        $searchHTML .= $this->getSearchFormHTML($this->query);
83
84
        $searchHTML .= $this->getSearchIntroHTML($this->query);
85
86
        $searchHTML .= $this->getPageLookupHTML($this->pageLookupResults);
87
88
        $searchHTML .= $this->getFulltextResultsHTML($this->fullTextResults, $this->highlight);
89
90
        echo $searchHTML;
91
    }
92
93
    /**
94
     * Get a form which can be used to adjust/refine the search
95
     *
96
     * @param string $query
97
     *
98
     * @return string
99
     */
100
    protected function getSearchFormHTML($query)
101
    {
102
        global $lang, $ID, $INPUT;
103
104
        $searchForm = (new Form())->attrs(['method' => 'get'])->addClass('search-results-form');
105
        $searchForm->setHiddenField('do', 'search');
106
        $searchForm->setHiddenField('id', $ID);
107
        $searchForm->setHiddenField('searchPageForm', '1');
108
        if ($INPUT->has('after')) {
109
            $searchForm->setHiddenField('after', $INPUT->str('after'));
110
        }
111
        if ($INPUT->has('before')) {
112
            $searchForm->setHiddenField('before', $INPUT->str('before'));
113
        }
114
        $searchForm->addFieldsetOpen()->addClass('search-results-form__fieldset');
115
        $searchForm->addTextInput('q')->val($query)->useInput(false);
116
        $searchForm->addButton('', $lang['btn_search'])->attr('type', 'submit');
117
118
        if ($this->isSearchAssistanceAvailable($this->parsedQuery)) {
119
            $this->addSearchAssistanceElements($searchForm, $this->parsedQuery);
120
        } else {
121
            $searchForm->addClass('search-results-form--no-assistance');
122
            $searchForm->addTagOpen('span')->addClass('search-results-form__no-assistance-message');
123
            $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.');
124
            $searchForm->addTagClose('span');
125
        }
126
127
        $searchForm->addFieldsetClose();
128
129
        trigger_event('SEARCH_FORM_DISPLAY', $searchForm);
130
131
        return $searchForm->toHTML();
132
    }
133
134
    /**
135
     * Decide if the given query is simple enough to provide search assistance
136
     *
137
     * @param array $parsedQuery
138
     *
139
     * @return bool
140
     */
141
    protected function isSearchAssistanceAvailable(array $parsedQuery)
142
    {
143
        if (count($parsedQuery['words']) > 1) {
144
            return false;
145
        }
146
        if (!empty($parsedQuery['not'])) {
147
            return false;
148
        }
149
150
        if (!empty($parsedQuery['phrases'])) {
151
            return false;
152
        }
153
154
        if (!empty($parsedQuery['notns'])) {
155
            return false;
156
        }
157
        if (count($parsedQuery['ns']) > 1) {
158
            return false;
159
        }
160
161
        return true;
162
    }
163
164
    /**
165
     * Add the elements to be used for search assistance
166
     *
167
     * @param Form  $searchForm
168
     * @param array $parsedQuery
169
     */
170
    protected function addSearchAssistanceElements(Form $searchForm, array $parsedQuery)
171
    {
172
        $searchForm->addButton('toggleAssistant', 'toggle search assistant')
173
            ->attr('type', 'button')
174
            ->id('search-results-form__show-assistance-button')
175
            ->addClass('search-results-form__show-assistance-button');
176
177
        $searchForm->addTagOpen('div')
178
            ->addClass('js-advancedSearchOptions')
179
            ->attr('style', 'display: none;');
180
181
        $this->addFragmentBehaviorLinks($searchForm, $parsedQuery);
182
        $this->addNamespaceSelector($searchForm, $parsedQuery);
183
        $this->addDateSelector($searchForm, $parsedQuery);
184
185
        $searchForm->addTagClose('div');
186
    }
187
188
    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...
189
    {
190
        $searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
191
        $searchForm->addHTML('fragment behavior: ');
192
193
        $this->addSearchLink(
194
            $searchForm,
195
            'exact match',
196
            array_map(function($term){return trim($term, '*');},$this->parsedQuery['and'])
197
        );
198
199
        $searchForm->addHTML(', ');
200
201
        $this->addSearchLink(
202
            $searchForm,
203
            'starts with',
204
            array_map(function($term){return trim($term, '*') . '*';},$this->parsedQuery['and'])
205
        );
206
207
        $searchForm->addHTML(', ');
208
209
        $this->addSearchLink(
210
            $searchForm,
211
            'ends with',
212
            array_map(function($term){return '*' . trim($term, '*');},$this->parsedQuery['and'])
213
        );
214
215
        $searchForm->addHTML(', ');
216
217
        $this->addSearchLink(
218
            $searchForm,
219
            'contains',
220
            array_map(function($term){return '*' . trim($term, '*') . '*';},$this->parsedQuery['and'])
221
        );
222
223
        $searchForm->addTagClose('div');
224
    }
225
226
    protected function addSearchLink(
227
        Form $searchForm,
228
        $label,
229
        array $and = null,
230
        array $ns = null,
231
        array $not = null,
232
        array $notns = null,
233
        array $phrases = null,
234
        $after = null,
235
        $before = null
236
    ) {
237
        global $INPUT, $ID;
238
        if (null === $and) {
239
            $and = $this->parsedQuery['and'];
240
        }
241
        if (null === $ns) {
242
            $ns = $this->parsedQuery['ns'];
243
        }
244
        if (null === $not) {
245
            $not = $this->parsedQuery['not'];
246
        }
247
        if (null === $phrases) {
248
            $phrases = $this->parsedQuery['phrases'];
249
        }
250
        if (null === $notns) {
251
            $notns = $this->parsedQuery['notns'];
252
        }
253
        if (null === $after) {
254
            $after = $INPUT->str('after');
255
        }
256
        if (null === $before) {
257
            $before = $INPUT->str('before');
258
        }
259
260
        $newQuery = ft_queryUnparser_simple(
261
            $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...
262
            $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...
263
            $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...
264
            $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...
265
            $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...
266
        );
267
        $hrefAttributes = ['do' => 'search', 'searchPageForm' => '1', 'q' => $newQuery];
268
        if ($after) {
269
            $hrefAttributes['after'] = $after;
270
        }
271
        if ($before) {
272
            $hrefAttributes['before'] = $before;
273
        }
274
        $searchForm->addTagOpen('a')
275
            ->attrs([
276
                'href' => wl($ID, $hrefAttributes, false, '&')
277
            ])
278
        ;
279
        $searchForm->addHTML($label);
280
        $searchForm->addTagClose('a');
281
    }
282
283
    /**
284
     * Add the elements for the namespace selector
285
     *
286
     * @param Form  $searchForm
287
     * @param array $parsedQuery
288
     */
289
    protected function addNamespaceSelector(Form $searchForm, array $parsedQuery)
290
    {
291
        $baseNS = empty($parsedQuery['ns']) ? '' : $parsedQuery['ns'][0];
292
        $searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
293
294
        $extraNS = $this->getAdditionalNamespacesFromResults($baseNS);
295
        if (!empty($extraNS) || $baseNS) {
296
            $searchForm->addTagOpen('div');
297
            $searchForm->addHTML('limit to namespace: ');
298
299
            if ($baseNS) {
300
                $this->addSearchLink(
301
                    $searchForm,
302
                    '(remove limit)',
303
                    null,
304
                    [],
305
                    null,
306
                    []
307
                );
308
            }
309
310
            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...
311
                $searchForm->addHTML(' ');
312
                $label = $extraNS . ($count ? " ($count)" : '');
313
314
                $this->addSearchLink($searchForm, $label, null, [$extraNS], null, []);
315
            }
316
            $searchForm->addTagClose('div');
317
        }
318
319
        $searchForm->addTagClose('div');
320
    }
321
322
    /**
323
     * Parse the full text results for their top namespaces below the given base namespace
324
     *
325
     * @param string $baseNS the namespace within which was searched, empty string for root namespace
326
     *
327
     * @return array an associative array with namespace => #number of found pages, sorted descending
328
     */
329
    protected function getAdditionalNamespacesFromResults($baseNS)
330
    {
331
        $namespaces = [];
332
        $baseNSLength = strlen($baseNS);
333
        foreach ($this->fullTextResults as $page => $numberOfHits) {
334
            $namespace = getNS($page);
335
            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...
336
                continue;
337
            }
338
            if ($namespace === $baseNS) {
339
                continue;
340
            }
341
            $firstColon = strpos((string)$namespace, ':', $baseNSLength + 1) ?: strlen($namespace);
342
            $subtopNS = substr($namespace, 0, $firstColon);
343
            if (empty($namespaces[$subtopNS])) {
344
                $namespaces[$subtopNS] = 0;
345
            }
346
            $namespaces[$subtopNS] += 1;
347
        }
348
        arsort($namespaces);
349
        return $namespaces;
350
    }
351
352
    /**
353
     * @ToDo: we need to remember this date when clicking on other links
354
     * @ToDo: custom date input
355
     *
356
     * @param Form $searchForm
357
     * @param      $parsedQuery
358
     */
359
    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...
360
        $searchForm->addTagOpen('div')->addClass('search-results-form__subwrapper');
361
        $searchForm->addHTML('limit by date: ');
362
363
        global $INPUT;
364
        if ($INPUT->has('before') || $INPUT->has('after')) {
365
            $this->addSearchLink(
366
                $searchForm,
367
                '(remove limit)',
368
                null,
369
                null,
370
                null,
371
                null,
372
                null,
373
                false,
374
                false
375
            );
376
377
            $searchForm->addHTML(', ');
378
        }
379
380
        if ($INPUT->str('after') === '1 week ago') {
381
            $searchForm->addHTML('<span class="active">past 7 days</span>');
382
        } else {
383
            $this->addSearchLink(
384
                $searchForm,
385
                'past 7 days',
386
                null,
387
                null,
388
                null,
389
                null,
390
                null,
391
                '1 week ago',
392
                false
393
            );
394
        }
395
396
        $searchForm->addHTML(', ');
397
398
        if ($INPUT->str('after') === '1 month ago') {
399
            $searchForm->addHTML('<span class="active">past month</span>');
400
        } else {
401
            $this->addSearchLink(
402
                $searchForm,
403
                'past month',
404
                null,
405
                null,
406
                null,
407
                null,
408
                null,
409
                '1 month ago',
410
                false
411
            );
412
        }
413
414
        $searchForm->addHTML(', ');
415
416
        if ($INPUT->str('after') === '1 year ago') {
417
            $searchForm->addHTML('<span class="active">past year</span>');
418
        } else {
419
            $this->addSearchLink(
420
                $searchForm,
421
                'past year',
422
                null,
423
                null,
424
                null,
425
                null,
426
                null,
427
                '1 year ago',
428
                false
429
            );
430
        }
431
432
        $searchForm->addTagClose('div');
433
    }
434
435
436
    /**
437
     * Build the intro text for the search page
438
     *
439
     * @param string $query the search query
440
     *
441
     * @return string
442
     */
443
    protected function getSearchIntroHTML($query)
444
    {
445
        global $ID, $lang;
446
447
        $intro = p_locale_xhtml('searchpage');
448
        // allow use of placeholder in search intro
449
        $pagecreateinfo = (auth_quickaclcheck($ID) >= AUTH_CREATE) ? $lang['searchcreatepage'] : '';
450
        $intro = str_replace(
451
            array('@QUERY@', '@SEARCH@', '@CREATEPAGEINFO@'),
452
            array(hsc(rawurlencode($query)), hsc($query), $pagecreateinfo),
453
            $intro
454
        );
455
        return $intro;
456
    }
457
458
    /**
459
     * Build HTML for a list of pages with matching pagenames
460
     *
461
     * @param array $data search results
462
     *
463
     * @return string
464
     */
465
    protected function getPageLookupHTML($data)
466
    {
467
        if (empty($data)) {
468
            return '';
469
        }
470
471
        global $lang;
472
473
        $html = '<div class="search_quickresult">';
474
        $html .= '<h3>' . $lang['quickhits'] . ':</h3>';
475
        $html .= '<ul class="search_quickhits">';
476
        foreach ($data as $id => $title) {
477
            $link = html_wikilink(':' . $id);
478
            $eventData = [
479
                'listItemContent' => [$link],
480
                'page' => $id,
481
            ];
482
            trigger_event('SEARCH_RESULT_PAGELOOKUP', $eventData);
483
            $html .= '<li>' . implode('', $eventData['listItemContent']) . '</li>';
484
        }
485
        $html .= '</ul> ';
486
        //clear float (see http://www.complexspiral.com/publications/containing-floats/)
487
        $html .= '<div class="clearer"></div>';
488
        $html .= '</div>';
489
490
        return $html;
491
    }
492
493
    /**
494
     * Build HTML for fulltext search results or "no results" message
495
     *
496
     * @param array $data      the results of the fulltext search
497
     * @param array $highlight the terms to be highlighted in the results
498
     *
499
     * @return string
500
     */
501
    protected function getFulltextResultsHTML($data, $highlight)
502
    {
503
        global $lang;
504
505
        if (empty($data)) {
506
            return '<div class="nothing">' . $lang['nothingfound'] . '</div>';
507
        }
508
509
        $html = '';
510
        $html .= '<dl class="search_results">';
511
        $num = 1;
512
513
        foreach ($data as $id => $cnt) {
514
            $resultLink = html_wikilink(':' . $id, null, $highlight);
515
516
            $resultHeader = [$resultLink];
517
518
519
            $restrictQueryToNSLink = $this->restrictQueryToNSLink(getNS($id));
520
            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...
521
                $resultHeader[] = $restrictQueryToNSLink;
522
            }
523
524
            $snippet = '';
525
            $lastMod = '';
526
            $mtime = filemtime(wikiFN($id));
527
            if ($cnt !== 0) {
528
                $resultHeader[] = $cnt . ' ' . $lang['hits'];
529
                if ($num < FT_SNIPPET_NUMBER) { // create snippets for the first number of matches only
530
                    $snippet = '<dd>' . ft_snippet($id, $highlight) . '</dd>';
531
                    $lastMod = '<span class="search_results__lastmod">'. $lang['lastmod'] . ' ';
532
                    $lastMod .= '<time datetime="' . date_iso8601($mtime) . '">'. dformat($mtime) . '</time>';
533
                    $lastMod .= '</span>';
534
                }
535
                $num++;
536
            }
537
538
            $metaLine = '<div class="search_results__metaLine">';
539
            $metaLine .= $lastMod;
540
            $metaLine .= '</div>';
541
542
543
            $eventData = [
544
                'resultHeader' => $resultHeader,
545
                'resultBody' => [$metaLine, $snippet],
546
                'page' => $id,
547
            ];
548
            trigger_event('SEARCH_RESULT_FULLPAGE', $eventData);
549
            $html .= '<div class="search_fullpage_result">';
550
            $html .= '<dt>' . implode(' ', $eventData['resultHeader']) . '</dt>';
551
            $html .= implode('', $eventData['resultBody']);
552
            $html .= '</div>';
553
        }
554
        $html .= '</dl>';
555
556
        return $html;
557
    }
558
559
    /**
560
     * create a link to restrict the current query to a namespace
561
     *
562
     * @param bool|string $ns the namespace to which to restrict the query
563
     *
564
     * @return bool|string
565
     */
566
    protected function restrictQueryToNSLink($ns)
567
    {
568
        if (!$ns) {
569
            return false;
570
        }
571
        if (!$this->isSearchAssistanceAvailable($this->parsedQuery)) {
572
            return false;
573
        }
574
        if (!empty($this->parsedQuery['ns']) && $this->parsedQuery['ns'][0] === $ns) {
575
            return false;
576
        }
577
        $name = '@' . $ns;
578
        $tmpForm = new Form();
579
        $this->addSearchLink($tmpForm, $name, null, [$ns], null, []);
580
        return $tmpForm->toHTML();
581
    }
582
}
583