1<?php
2
3use dokuwiki\Extension\Plugin;
4
5/**
6 * DokuWiki Plugin searchns (Helper Component)
7 *
8 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
9 * @author Anna Dabrowska <dokuwiki@cosmocode.de>
10 */
11class helper_plugin_searchns extends Plugin
12{
13    public const SEARCHNS_HEADING = 'SEARCHNS_HEADING';
14
15    /**
16     * @var array
17     */
18    protected $ns;
19
20    /**
21     * Constructor
22     */
23    public function __construct()
24    {
25        $this->ns = $this->getNsFromConfig();
26    }
27
28    /**
29     * Converts config string to array
30     *
31     * @return array
32     */
33    public function getNsFromConfig()
34    {
35        if (!is_null($this->ns)) {
36            return $this->ns;
37        }
38
39        $ns = [];
40        $config = $this->getConf('namespaces');
41
42        if (empty($config)) return $ns;
43
44        $lines = array_filter(explode("\n", $config));
45        foreach ($lines as $line) {
46            $n = sexplode(' ', $line, 2);
47            if (strpos($n[0], ':', -1) === false) {
48                msg('Search namespaces are not configured correctly!', -1);
49                return $ns;
50            }
51            $ns[trim($n[1])] = rtrim(trim($n[0], ':'));
52        }
53
54        if ($this->getConf('first all')) {
55            $ns = array_merge([$this->getLang('all label') => ''], $ns);
56        } else {
57            $ns[$this->getLang('all label')] = '';
58        }
59
60        return $ns;
61    }
62
63    /**
64     * array_filter callback to remove namespaces that the current user can't read
65     *
66     * @param string $ns
67     * @return bool
68     */
69    public function filterByACL($ns)
70    {
71        if ($ns === '') return true; // all is always allowed
72        return auth_quickaclcheck($ns . ':*') >= AUTH_READ;
73    }
74
75    /**
76     * Returns HTML list of search results.
77     * Based on core quicksearch
78     * @see \dokuwiki\Ajax::callQsearch()
79     *
80     * @return string
81     */
82    public function qSearch()
83    {
84        global $INPUT;
85
86        $maxnumbersuggestions = 50;
87
88        // search parameters as posted via AJAX
89        $query = $INPUT->str('q');
90        $ns = $INPUT->str('ns');
91        if (empty($query)) return '';
92
93        $query = urldecode($query) . ($ns ? " @$ns" : '');
94        $data = ft_pageLookup($query, true, useHeading('navigation'));
95
96        if ($data === []) return '';
97
98        $ret = '<ul>';
99
100        $counter = 0;
101        foreach ($data as $id => $title) {
102            if (useHeading('navigation')) {
103                $name = $title;
104            } elseif (!$ns) {
105                $linkNs = getNS($id);
106                if ($linkNs) {
107                    $name = noNS($id) . ' (' . $linkNs . ')';
108                } else {
109                    $name = $id;
110                }
111            } else {
112                $name = $id;
113            }
114            $ret .= '<li>';
115            $ret .= $title === self::SEARCHNS_HEADING ? $id : html_wikilink(':' . $id, $name);
116            $ret .= '</li>';
117
118            $counter++;
119            if ($counter > $maxnumbersuggestions) {
120                $ret .=  '<li>...</li>';
121                break;
122            }
123        }
124
125        $ret .= '</ul>';
126        return $ret;
127    }
128
129    /**
130     * Group results based on configured namespaces.
131     * Insert headings as pseudo results.
132     *
133     * @param $results
134     * @return array
135     */
136    public function sortAndGroup($results)
137    {
138        $namespaces = array_filter($this->getNsFromConfig());
139        $original = $namespaces;
140
141        // make sure more specific namespaces are sorted first
142        uasort($namespaces, 'ft_pagesorter');
143        $namespaces = array_reverse($namespaces);
144
145        $res = [];
146        foreach ($namespaces as $label => $ns) {
147            $res[$ns] = array_filter(
148                $results,
149                static fn($page) => strpos($page, $ns . ':') === 0,
150                ARRAY_FILTER_USE_KEY
151            );
152
153            if (!empty($res[$ns])) {
154                // prepend namespace label
155                $res[$ns] = array_merge([$label => self::SEARCHNS_HEADING], $res[$ns]);
156                // remove matches from result set
157                $results = array_diff_key($results, $res[$ns]);
158            }
159        }
160
161        // reorder namespace as in the original configuration
162        $all = [];
163        foreach ($original as $ns) {
164            if (!empty($res[$ns])) {
165                $all = array_merge($all, $res[$ns]);
166            }
167        }
168
169        // add the remainder as "other"
170        $rest = array_diff_key($results, $all);
171        if ($rest !== []) {
172            $all[$this->getLang('other label')] = self::SEARCHNS_HEADING;
173            $all = array_merge($all, $rest);
174        }
175
176        return $all;
177    }
178}
179