xref: /dokuwiki/inc/Search/MetadataSearch.php (revision fab81cc8d9f8ae6dbaa16254159007a2d6d9a8d0)
1<?php
2
3namespace dokuwiki\Search;
4
5use dokuwiki\Extension\Event;
6use dokuwiki\Search\MetadataIndex;
7use dokuwiki\Search\QueryParser;
8
9/**
10 * Class DokuWiki Metadata Search
11 *
12 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
13 * @author     Andreas Gohr <andi@splitbrain.org>
14 */
15class MetadataSearch
16{
17    /**
18     * Quicksearch for pagenames
19     *
20     * By default it only matches the pagename and ignores the namespace.
21     * This can be changed with the second parameter.
22     * The third parameter allows to search in titles as well.
23     *
24     * The function always returns titles as well
25     *
26     * @triggers SEARCH_QUERY_PAGELOOKUP
27     * @author   Andreas Gohr <andi@splitbrain.org>
28     * @author   Adrian Lang <lang@cosmocode.de>
29     *
30     * @param string     $id       page id
31     * @param bool       $in_ns    match against namespace as well?
32     * @param bool       $in_title search in title?
33     * @param int|string $after    only show results with mtime after this date,
34     *                             accepts timestap or strtotime arguments
35     * @param int|string $before   only show results with mtime before this date,
36     *                             accepts timestap or strtotime arguments
37     *
38     * @return string[]
39     */
40    public function pageLookup($id, $in_ns = false, $in_title = false, $after = null, $before = null)
41    {
42        $data = [
43            'id' => $id,
44            'in_ns' => $in_ns,
45            'in_title' => $in_title,
46            'after' => $after,
47            'before' => $before
48        ];
49        $data['has_titles'] = true; // for plugin backward compatibility check
50        $action = [$this, 'pageLookupCallBack'];
51        return Event::createAndTrigger('SEARCH_QUERY_PAGELOOKUP', $data, $action);
52    }
53
54    /**
55     * Returns list of pages as array(pageid => First Heading)
56     *
57     * @param array $data  event data
58     * @return string[]
59     */
60    public function pageLookupCallBack(&$data)
61    {
62        // split out original parameters
63        $id = $data['id'];
64        $parsedQuery = (new QueryParser)->convert($id);
65
66        if (count($parsedQuery['ns']) > 0) {
67            $ns = cleanID($parsedQuery['ns'][0]) . ':';
68            $id = implode(' ', $parsedQuery['highlight']);
69        }
70        if (count($parsedQuery['notns']) > 0) {
71            $notns = cleanID($parsedQuery['notns'][0]) . ':';
72            $id = implode(' ', $parsedQuery['highlight']);
73        }
74
75        $in_ns    = $data['in_ns'];
76        $in_title = $data['in_title'];
77        $cleaned = cleanID($id);
78
79        $MetadataIndex = new MetadataIndex();
80        $page_idx = $MetadataIndex->getPages();
81
82        $pages = array();
83        if ($id !== '' && $cleaned !== '') {
84            foreach ($page_idx as $p_id) {
85                if ((strpos($in_ns ? $p_id : noNSorNS($p_id), $cleaned) !== false)) {
86                    if (!isset($pages[$p_id])) {
87                        $pages[$p_id] = p_get_first_heading($p_id, METADATA_DONT_RENDER);
88                    }
89                }
90            }
91            if ($in_title) {
92                $func = [$this, 'pageLookupTitleCompare'];
93                foreach ($MetadataIndex->lookupKey('title', $id, $func) as $p_id) {
94                    if (!isset($pages[$p_id])) {
95                        $pages[$p_id] = p_get_first_heading($p_id, METADATA_DONT_RENDER);
96                    }
97                }
98            }
99        }
100
101        if (isset($ns)) {
102            foreach (array_keys($pages) as $p_id) {
103                if (strpos($p_id, $ns) !== 0) {
104                    unset($pages[$p_id]);
105                }
106            }
107        }
108        if (isset($notns)) {
109            foreach (array_keys($pages) as $p_id) {
110                if (strpos($p_id, $notns) === 0) {
111                    unset($pages[$p_id]);
112                }
113            }
114        }
115
116        // discard hidden pages
117        // discard nonexistent pages
118        // check ACL permissions
119        foreach (array_keys($pages) as $idx) {
120            if (!isVisiblePage($idx) || !page_exists($idx) || auth_quickaclcheck($idx) < AUTH_READ) {
121                unset($pages[$idx]);
122            }
123        }
124
125        $pages = $this->filterResultsByTime($pages, $data['after'], $data['before']);
126
127        uksort($pages, [$this, 'pagesorter']);
128        return $pages;
129    }
130
131    /**
132     * Tiny helper function for comparing the searched title with the title
133     * from the search index. This function is a wrapper around stripos with
134     * adapted argument order and return value.
135     *
136     * @param string $search searched title
137     * @param string $title  title from index
138     * @return bool
139     */
140    protected function pageLookupTitleCompare($search, $title)
141    {
142        return stripos($title, $search) !== false;
143    }
144
145    /**
146     * Sort pages based on their namespace level first, then on their string
147     * values. This makes higher hierarchy pages rank higher than lower hierarchy
148     * pages.
149     *
150     * @param string $a
151     * @param string $b
152     * @return int Returns < 0 if $a is less than $b; > 0 if $a is greater than $b,
153     *             and 0 if they are equal.
154     */
155    protected function pagesorter($a, $b)
156    {
157        $ac = count(explode(':',$a));
158        $bc = count(explode(':',$b));
159        if ($ac < $bc) {
160            return -1;
161        } elseif ($ac > $bc) {
162            return 1;
163        }
164        return strcmp ($a,$b);
165    }
166
167    /**
168     * @param array      $results search results in the form pageid => value
169     * @param int|string $after   only returns results with mtime after this date,
170     *                            accepts timestap or strtotime arguments
171     * @param int|string $before  only returns results with mtime after this date,
172     *                            accepts timestap or strtotime arguments
173     *
174     * @return array
175     */
176    protected function filterResultsByTime(array $results, $after, $before)
177    {
178        if ($after || $before) {
179            $after = is_int($after) ? $after : strtotime($after);
180            $before = is_int($before) ? $before : strtotime($before);
181
182            foreach ($results as $id => $value) {
183                $mTime = filemtime(wikiFN($id));
184                if ($after && $after > $mTime) {
185                    unset($results[$id]);
186                    continue;
187                }
188                if ($before && $before < $mTime) {
189                    unset($results[$id]);
190                }
191            }
192        }
193        return $results;
194    }
195}
196