Issues (847)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

inc/Ui/Search.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace dokuwiki\Ui;
4
5
use dokuwiki\Extension\Event;
6
use dokuwiki\Form\Form;
7
use dokuwiki\Utf8\Sort;
8
9
class Search extends Ui
10
{
11
    protected $query;
12
    protected $parsedQuery;
13
    protected $searchState;
14
    protected $pageLookupResults = array();
15
    protected $fullTextResults = array();
16
    protected $highlight = array();
17
18
    /**
19
     * Search constructor.
20
     *
21
     * @param array $pageLookupResults pagename lookup results in the form [pagename => pagetitle]
22
     * @param array $fullTextResults fulltext search results in the form [pagename => #hits]
23
     * @param array $highlight  array of strings to be highlighted
24
     */
25
    public function __construct(array $pageLookupResults, array $fullTextResults, $highlight)
26
    {
27
        global $QUERY;
28
        $Indexer = idx_get_indexer();
29
30
        $this->query = $QUERY;
31
        $this->parsedQuery = ft_queryParser($Indexer, $QUERY);
32
        $this->searchState = new SearchState($this->parsedQuery);
33
34
        $this->pageLookupResults = $pageLookupResults;
35
        $this->fullTextResults = $fullTextResults;
36
        $this->highlight = $highlight;
37
    }
38
39
    /**
40
     * display the search result
41
     *
42
     * @return void
43
     */
44
    public function show()
45
    {
46
        $searchHTML = '';
47
48
        $searchHTML .= $this->getSearchIntroHTML($this->query);
49
50
        $searchHTML .= $this->getSearchFormHTML($this->query);
51
52
        $searchHTML .= $this->getPageLookupHTML($this->pageLookupResults);
53
54
        $searchHTML .= $this->getFulltextResultsHTML($this->fullTextResults, $this->highlight);
55
56
        echo $searchHTML;
57
    }
58
59
    /**
60
     * Get a form which can be used to adjust/refine the search
61
     *
62
     * @param string $query
63
     *
64
     * @return string
65
     */
66
    protected function getSearchFormHTML($query)
67
    {
68
        global $lang, $ID, $INPUT;
69
70
        $searchForm = (new Form(['method' => 'get'], true))->addClass('search-results-form');
71
        $searchForm->setHiddenField('do', 'search');
72
        $searchForm->setHiddenField('id', $ID);
73
        $searchForm->setHiddenField('sf', '1');
74
        if ($INPUT->has('min')) {
75
            $searchForm->setHiddenField('min', $INPUT->str('min'));
76
        }
77
        if ($INPUT->has('max')) {
78
            $searchForm->setHiddenField('max', $INPUT->str('max'));
79
        }
80
        if ($INPUT->has('srt')) {
81
            $searchForm->setHiddenField('srt', $INPUT->str('srt'));
82
        }
83
        $searchForm->addFieldsetOpen()->addClass('search-form');
84
        $searchForm->addTextInput('q')->val($query)->useInput(false);
0 ignored issues
show
It seems like you code against a specific sub-type and not the parent class dokuwiki\Form\Element as the method useInput() does only exist in the following sub-classes of dokuwiki\Form\Element: dokuwiki\Form\CheckableElement, dokuwiki\Form\DropdownElement, dokuwiki\Form\InputElement, dokuwiki\Form\TextareaElement. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

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