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