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