1<?php
2
3use dokuwiki\Extension\Event;
4
5/**
6 * DokuWiki Plugin linksuggest (Action Component)
7 *
8 * ajax autosuggest for links
9 *
10 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
11 * @author lisps
12 */
13
14class action_plugin_linksuggest extends DokuWiki_Action_Plugin {
15
16    /**
17     * Register the eventhandlers
18     *
19     * @param Doku_Event_Handler $controller
20     */
21    public function register(Doku_Event_Handler $controller) {
22        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'page_link');
23        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'media_link');
24        $controller->register_hook('DOKUWIKI_STARTED', 'AFTER',  $this, '_add_config');
25    }
26    public function _add_config(&$event, $param) {
27        global $JSINFO;
28        $JSINFO['append_header'] = $this->getConf('append_header');
29    }
30
31    /**
32     * ajax Request Handler
33     * page_link
34     *
35     * @param $event
36     */
37    public function page_link($event) {
38        if ($event->data !== 'plugin_linksuggest') {
39            return;
40        }
41        //no other ajax call handlers needed
42        $event->stopPropagation();
43        $event->preventDefault();
44
45        global $INPUT;
46
47        //current page/ns
48        $current_pageid = trim($INPUT->post->str('id')); //current id
49        $current_ns = getNS($current_pageid);
50        $q = trim($INPUT->post->str('q')); //entered string
51
52        //keep hashlink if exists
53        list($q, $hash) = array_pad(explode('#', $q, 2), 2, null);
54
55        $has_hash = !($hash === null);
56        $entered_ns = getNS($q); //namespace of entered string
57        $trailing = ':'; //needs to be remembered, such that actual user input can be returned
58        if($entered_ns === false) {
59            //no namespace given (i.e. none : in $q)
60            // .xxx, ..xxx, ~xxx, if in front of ns, cleaned in $entered_page
61            if (substr($q, 0, 2) === '..') {
62                $entered_ns = '..';
63            } elseif (substr($q, 0, 1) === '.') {
64                $entered_ns = '.';
65
66            } elseif (substr($q, 0, 1) === '~') {
67                $entered_ns = '~';
68            }
69            $trailing = '';
70        }
71
72        $entered_page = cleanID(noNS($q)); //page part of entered string
73
74        if ($entered_ns === '') { // [[:xxx -> absolute link
75            $matchedPages = $this->search_pages('', $entered_page, $has_hash);
76        } else if (strpos($q, '.') !== false //relative link (., .:, .., ..:, .ns: etc, and :..:, :.: )
77            || substr($entered_ns, 0, 1) == '~') { // ~, ~:,
78            //resolve the ns based on current id
79            $ns = $entered_ns;
80            if($entered_ns === '~') {
81                //add a random page name, otherwise it ~ or ~: are interpret as ~:start
82                $ns .= 'uniqueadditionforlinksuggestplugin';
83            }
84
85            if (class_exists('dokuwiki\File\PageResolver')) {
86                // Igor and later
87                $resolver = new dokuwiki\File\PageResolver($current_pageid);
88                $resolved_ns = $resolver->resolveId($ns);
89            } else {
90                // Compatibility with older releases
91                $resolved_ns = $ns;
92                resolve_pageid(getNS($current_pageid), $resolved_ns, $exists);
93            }
94            if($entered_ns === '~') {
95                $resolved_ns = substr($resolved_ns, 0,-35); //remove : and unique string
96            }
97
98            $matchedPages = $this->search_pages($resolved_ns, $entered_page, $has_hash);
99        } else if ($entered_ns === false && $current_ns) { // [[xxx while current page not in root-namespace
100            $matchedPages = array_merge(
101                $this->search_pages($current_ns, $entered_page, true),//search in current for pages
102                $this->search_pages('', $entered_page, $has_hash)           //search in root both pgs and ns
103            );
104        } else {
105            $matchedPages = $this->search_pages($entered_ns, $entered_page, $has_hash);
106        }
107
108        $data_suggestions = [];
109        $link = '';
110
111        if ($hash !== null && $matchedPages[0]['type'] === 'f') {
112            //if hash is given and a page was found
113            $page = $matchedPages[0]['id'];
114            $meta = p_get_metadata($page, false, METADATA_RENDER_USING_CACHE);
115
116            if (isset($meta['internal']['toc'])) {
117                $toc = $meta['description']['tableofcontents'];
118                Event::createAndTrigger('TPL_TOC_RENDER', $toc, null, false);
119                if (is_array($toc) && count($toc) !== 0) {
120                    foreach ($toc as $t) { //loop through toc and compare
121                        if ($hash === '' || strpos($t['hid'], $hash) === 0) {
122                            $data_suggestions[] = $t;
123                        }
124                    }
125                    $link = $q;
126                }
127            }
128        } else {
129
130            foreach ($matchedPages as $entry) {
131                //a page in rootns
132                if($current_ns !== '' && !$entry['ns'] && $entry['type'] === 'f') {
133                    $trailing = ':';
134                }
135
136                $data_suggestions[] = [
137                    'id' => noNS($entry['id']),
138                    //return literally ns what user has typed in before page name/namespace name that is suggested
139                    'ns' => $entered_ns . $trailing,
140                    'type' => $entry['type'], // d/f
141                    'title' => $entry['title'] ?? '', //namespace have no title, for pages sometimes no title
142                    'rootns' => $entry['ns'] ? 0 : 1,
143                ];
144            }
145        }
146
147        echo json_encode([
148            'data' => $data_suggestions,
149            'link' => $link
150        ]);
151    }
152
153    /**
154     * ajax Request Handler
155     * media_link
156     *
157     * @param Event $event
158     */
159    public function media_link($event) {
160        if ($event->data !== 'plugin_imglinksuggest') {
161            return;
162        }
163        //no other ajax call handlers needed
164        $event->stopPropagation();
165        $event->preventDefault();
166
167        global $INPUT;
168
169        //current media/ns
170        $current_pageid = trim($INPUT->post->str('id')); //current id
171        $current_ns = getNS($current_pageid);
172        $q = trim($INPUT->post->str('q')); //entered string
173
174        $entered_ns = getNS($q); //namespace of entered string
175        $trailing = ':'; //needs to be remembered, such that actual user input can be returned
176        if($entered_ns === false) {
177            //no namespace given (i.e. none : in $q)
178            // .xxx, ..xxx, ~xxx, if in front of ns, cleaned in $entered_page
179            if (substr($q, 0, 2) === '..') {
180                $entered_ns = '..';
181            } elseif (substr($q, 0, 1) === '.') {
182                $entered_ns = '.';
183
184            } elseif (substr($q, 0, 1) === '~') {
185                $entered_ns = '~';
186            }
187            $trailing = '';
188        }
189
190        $entered_media = cleanID(noNS($q)); //page part of entered string
191
192        if ($entered_ns === '') { // [[:xxx -> absolute link
193            $matchedMedias = $this->search_medias('', $entered_media);
194        } else if (strpos($q, '.') !== false //relative link (., .:, .., ..:, .ns: etc, and :..:, :.: )
195            || substr($entered_ns, 0, 1) == '~') { // ~, ~:,
196            //resolve the ns based on current id
197            $ns = $entered_ns;
198            if($entered_ns === '~') {
199                //add a random page name, otherwise it ~ or ~: are interpret as ~:start
200                $ns .= 'uniqueadditionforlinksuggestplugin';
201            }
202
203            if (class_exists('dokuwiki\File\PageResolver')) {
204                // Igor and later
205                $resolver = new dokuwiki\File\MediaResolver($current_pageid);
206                $resolved_ns = $resolver->resolveId($ns);
207            } else {
208                // Compatibility with older releases
209                $resolved_ns = $ns;
210                resolve_mediaid(getNS($current_pageid), $resolved_ns, $exists);
211            }
212            if($entered_ns === '~') {
213                $resolved_ns = substr($resolved_ns, 0,-35); //remove : and unique string
214            }
215
216            $matchedMedias = $this->search_medias($resolved_ns, $entered_media);
217        } else if ($entered_ns === false && $current_ns) { // [[xxx while current page not in root-namespace
218            $matchedMedias = array_merge(
219                $this->search_medias($current_ns, $entered_media), //search in current for pages
220                $this->search_medias('', $entered_media)       //search in root both pgs and ns
221            );
222        } else {
223            $matchedMedias = $this->search_medias($entered_ns, $entered_media);
224        }
225
226        $data_suggestions = [];
227        foreach ($matchedMedias as $entry) {
228            //a page in rootns
229            if($current_ns !== '' && !$entry['ns'] && $entry['type'] === 'f') {
230                $trailing = ':';
231            }
232
233            $data_suggestions[] = [
234                'id' => noNS($entry['id']),
235                //return literally ns what user has typed in before page name/namespace name that is suggested
236                'ns' => $entered_ns . $trailing,
237                'type' => $entry['type'], // d/f
238                'rootns' => $entry['ns'] ? 0 : 1,
239            ];
240        }
241
242        echo json_encode([
243            'data' => $data_suggestions,
244            'link' => ''
245        ]);
246    }
247
248
249    /**
250     * List available pages, and eventually namespaces
251     *
252     * @param string $ns namespace to search in
253     * @param string $id
254     * @param bool $pagesonly true: pages only, false: pages and namespaces
255     * @return array
256     */
257    protected function search_pages($ns, $id, $pagesonly = false) {
258        global $conf;
259
260        $data = [];
261        $nsd = utf8_encodeFN(str_replace(':', '/', $ns)); //dir
262
263        $opts = [
264            'depth' => 1,
265            'listfiles' => true,
266            'listdirs' => !$pagesonly,
267            'pagesonly' => true,
268            'firsthead' => true,
269            'sneakyacl' => $conf['sneaky_index'],
270        ];
271        if ($id) {
272            $opts['filematch'] = '^.*\/' . $id;
273        }
274        if ($id && !$pagesonly) {
275            $opts['dirmatch'] = '^.*\/' . $id;
276        }
277        search($data, $conf['datadir'], 'search_universal', $opts, $nsd);
278
279        return $data;
280    }
281
282    /**
283     * List available media
284     *
285     * @param string $ns
286     * @param string $id
287     * @return array
288     */
289    protected function search_medias($ns, $id) {
290        global $conf;
291
292        $data = [];
293        $nsd = utf8_encodeFN(str_replace(':', '/', $ns)); //dir
294
295        $opts = [
296            'depth' => 1,
297            'listfiles' => true,
298            'listdirs' => true,
299            'firsthead' => true,
300            'sneakyacl' => $conf['sneaky_index'],
301        ];
302        if ($id) {
303            $opts['filematch'] = '^.*\/' . $id;
304        }
305        if ($id) {
306            $opts['dirmatch'] = '^.*\/' . $id;
307        }
308        search($data, $conf['mediadir'], 'search_universal', $opts, $nsd);
309
310        return $data;
311    }
312
313}
314