xref: /dokuwiki/inc/Ajax.php (revision 8c7c53b0321a3cd3116b8d3b2ad27863a38dece7)
1<?php
2
3namespace dokuwiki;
4
5use dokuwiki\Extension\Event;
6use dokuwiki\Ui\MediaDiff;
7use dokuwiki\Ui\Index;
8use dokuwiki\Ui;
9use dokuwiki\Utf8\Sort;
10
11/**
12 * Manage all builtin AJAX calls
13 *
14 * @todo The calls should be refactored out to their own proper classes
15 * @package dokuwiki
16 */
17class Ajax
18{
19
20    /**
21     * Execute the given call
22     *
23     * @param string $call name of the ajax call
24     */
25    public function __construct($call) {
26        $callfn = 'call' . ucfirst($call);
27        if(method_exists($this, $callfn)) {
28            $this->$callfn();
29        } else {
30            $evt = new Event('AJAX_CALL_UNKNOWN', $call);
31            if($evt->advise_before()) {
32                print "AJAX call '" . hsc($call) . "' unknown!\n";
33            } else {
34                $evt->advise_after();
35                unset($evt);
36            }
37        }
38    }
39
40    /**
41     * Searches for matching pagenames
42     *
43     * @author Andreas Gohr <andi@splitbrain.org>
44     */
45    protected function callQsearch() {
46        global $lang;
47        global $INPUT;
48
49        $maxnumbersuggestions = 50;
50
51        $query = $INPUT->post->str('q');
52        if(empty($query)) $query = $INPUT->get->str('q');
53        if(empty($query)) return;
54
55        $query = urldecode($query);
56
57        $data = ft_pageLookup($query, true, useHeading('navigation'));
58
59        if($data === []) return;
60
61        print '<strong>' . $lang['quickhits'] . '</strong>';
62        print '<ul>';
63        $counter = 0;
64        foreach($data as $id => $title) {
65            if(useHeading('navigation')) {
66                $name = $title;
67            } else {
68                $ns = getNS($id);
69                if($ns) {
70                    $name = noNS($id) . ' (' . $ns . ')';
71                } else {
72                    $name = $id;
73                }
74            }
75            echo '<li>' . html_wikilink(':' . $id, $name) . '</li>';
76
77            $counter++;
78            if($counter > $maxnumbersuggestions) {
79                echo '<li>...</li>';
80                break;
81            }
82        }
83        print '</ul>';
84    }
85
86    /**
87     * Support OpenSearch suggestions
88     *
89     * @link   http://www.opensearch.org/Specifications/OpenSearch/Extensions/Suggestions/1.0
90     * @author Mike Frysinger <vapier@gentoo.org>
91     */
92    protected function callSuggestions() {
93        global $INPUT;
94
95        $query = cleanID($INPUT->post->str('q'));
96        if(empty($query)) $query = cleanID($INPUT->get->str('q'));
97        if(empty($query)) return;
98
99        $data = ft_pageLookup($query);
100        if($data === []) return;
101        $data = array_keys($data);
102
103        // limit results to 15 hits
104        $data = array_slice($data, 0, 15);
105        $data = array_map('trim', $data);
106        $data = array_map('noNS', $data);
107        $data = array_unique($data);
108        Sort::sort($data);
109
110        /* now construct a json */
111        $suggestions = [
112            $query, // the original query
113            $data, // some suggestions
114            [], // no description
115            [], // no urls
116        ];
117
118        header('Content-Type: application/x-suggestions+json');
119        print json_encode($suggestions, JSON_THROW_ON_ERROR);
120    }
121
122    /**
123     * Refresh a page lock and save draft
124     *
125     * Andreas Gohr <andi@splitbrain.org>
126     */
127    protected function callLock() {
128        global $ID;
129        global $INFO;
130        global $INPUT;
131
132        $ID = cleanID($INPUT->post->str('id'));
133        if(empty($ID)) return;
134
135        $INFO = pageinfo();
136
137        $response = [
138            'errors' => [],
139            'lock' => '0',
140            'draft' => '',
141        ];
142        if(!$INFO['writable']) {
143            $response['errors'][] = 'Permission to write this page has been denied.';
144            echo json_encode($response);
145            return;
146        }
147
148        if(!checklock($ID)) {
149            lock($ID);
150            $response['lock'] = '1';
151        }
152
153        $draft = new Draft($ID, $INFO['client']);
154        if ($draft->saveDraft()) {
155            $response['draft'] = $draft->getDraftMessage();
156        } else {
157            $response['errors'] = array_merge($response['errors'], $draft->getErrors());
158        }
159        echo json_encode($response, JSON_THROW_ON_ERROR);
160    }
161
162    /**
163     * Delete a draft
164     *
165     * @author Andreas Gohr <andi@splitbrain.org>
166     */
167    protected function callDraftdel() {
168        global $INPUT;
169        $id = cleanID($INPUT->str('id'));
170        if(empty($id)) return;
171
172        $client = $INPUT->server->str('REMOTE_USER');
173        if(!$client) $client = clientIP(true);
174
175        $draft = new Draft($id, $client);
176        if ($draft->isDraftAvailable() && checkSecurityToken()) {
177            $draft->deleteDraft();
178        }
179    }
180
181    /**
182     * Return subnamespaces for the Mediamanager
183     *
184     * @author Andreas Gohr <andi@splitbrain.org>
185     */
186    protected function callMedians() {
187        global $conf;
188        global $INPUT;
189
190        // wanted namespace
191        $ns = cleanID($INPUT->post->str('ns'));
192        $dir = utf8_encodeFN(str_replace(':', '/', $ns));
193
194        $lvl = count(explode(':', $ns));
195
196        $data = [];
197        search($data, $conf['mediadir'], 'search_index', ['nofiles' => true], $dir);
198        foreach(array_keys($data) as $item) {
199            $data[$item]['level'] = $lvl + 1;
200        }
201        echo html_buildlist($data, 'idx', 'media_nstree_item', 'media_nstree_li');
202    }
203
204    /**
205     * Return list of files for the Mediamanager
206     *
207     * @author Andreas Gohr <andi@splitbrain.org>
208     */
209    protected function callMedialist() {
210        global $NS;
211        global $INPUT;
212
213        $NS = cleanID($INPUT->post->str('ns'));
214        $sort = $INPUT->post->bool('recent') ? 'date' : 'natural';
215        if($INPUT->post->str('do') == 'media') {
216            tpl_mediaFileList();
217        } else {
218            tpl_mediaContent(true, $sort);
219        }
220    }
221
222    /**
223     * Return the content of the right column
224     * (image details) for the Mediamanager
225     *
226     * @author Kate Arzamastseva <pshns@ukr.net>
227     */
228    protected function callMediadetails() {
229        global $IMG, $JUMPTO, $REV, $fullscreen, $INPUT;
230        $fullscreen = true;
231        require_once(DOKU_INC . 'lib/exe/mediamanager.php');
232
233        $image = '';
234        if($INPUT->has('image')) $image = cleanID($INPUT->str('image'));
235        if(isset($IMG)) $image = $IMG;
236        if(isset($JUMPTO)) $image = $JUMPTO;
237        $rev = false;
238        if(isset($REV) && !$JUMPTO) $rev = $REV;
239
240        html_msgarea();
241        tpl_mediaFileDetails($image, $rev);
242    }
243
244    /**
245     * Returns image diff representation for mediamanager
246     *
247     * @author Kate Arzamastseva <pshns@ukr.net>
248     */
249    protected function callMediadiff() {
250        global $INPUT;
251
252        $image = '';
253        if($INPUT->has('image')) $image = cleanID($INPUT->str('image'));
254        (new MediaDiff($image))->preference('fromAjax', true)->show();
255    }
256
257    /**
258     * Manages file uploads
259     *
260     * @author Kate Arzamastseva <pshns@ukr.net>
261     */
262    protected function callMediaupload() {
263        global $NS, $MSG, $INPUT;
264
265        $id = '';
266        if(isset($_FILES['qqfile']['tmp_name'])) {
267            $id = $INPUT->post->str('mediaid', $_FILES['qqfile']['name']);
268        } elseif($INPUT->get->has('qqfile')) {
269            $id = $INPUT->get->str('qqfile');
270        }
271
272        $id = cleanID($id);
273
274        $NS = $INPUT->str('ns');
275        $ns = $NS . ':' . getNS($id);
276
277        $AUTH = auth_quickaclcheck("$ns:*");
278        if($AUTH >= AUTH_UPLOAD) {
279            io_createNamespace("$ns:xxx", 'media');
280        }
281
282        if(isset($_FILES['qqfile']['error']) && $_FILES['qqfile']['error']) unset($_FILES['qqfile']);
283
284        $res = false;
285        if(isset($_FILES['qqfile']['tmp_name'])) $res = media_upload($NS, $AUTH, $_FILES['qqfile']);
286        if($INPUT->get->has('qqfile')) $res = media_upload_xhr($NS, $AUTH);
287
288        if($res) {
289            $result = [
290                'success' => true,
291                'link' => media_managerURL(['ns' => $ns, 'image' => $NS . ':' . $id], '&'),
292                'id' => $NS . ':' . $id,
293                'ns' => $NS
294            ];
295        } else {
296            $error = '';
297            if(isset($MSG)) {
298                foreach($MSG as $msg) {
299                    $error .= $msg['msg'];
300                }
301            }
302            $result = ['error' => $error, 'ns' => $NS];
303        }
304
305        header('Content-Type: application/json');
306        echo json_encode($result, JSON_THROW_ON_ERROR);
307    }
308
309    /**
310     * Return sub index for index view
311     *
312     * @author Andreas Gohr <andi@splitbrain.org>
313     */
314    protected function callIndex() {
315        global $conf;
316        global $INPUT;
317
318        // wanted namespace
319        $ns = cleanID($INPUT->post->str('idx'));
320        $dir = utf8_encodeFN(str_replace(':', '/', $ns));
321
322        $lvl = count(explode(':', $ns));
323
324        $data = [];
325        search($data, $conf['datadir'], 'search_index', ['ns' => $ns], $dir);
326        foreach (array_keys($data) as $item) {
327            $data[$item]['level'] = $lvl + 1;
328        }
329        $idx = new Index;
330        echo html_buildlist($data, 'idx', [$idx,'formatListItem'], [$idx,'tagListItem']);
331    }
332
333    /**
334     * List matching namespaces and pages for the link wizard
335     *
336     * @author Andreas Gohr <gohr@cosmocode.de>
337     */
338    protected function callLinkwiz() {
339        global $conf;
340        global $lang;
341        global $INPUT;
342
343        $q = ltrim(trim($INPUT->post->str('q')), ':');
344        $id = noNS($q);
345        $ns = getNS($q);
346
347        $ns = cleanID($ns);
348
349        $id = cleanID($id);
350
351        $nsd = utf8_encodeFN(str_replace(':', '/', $ns));
352
353        $data = [];
354        if($q !== '' && $ns === '') {
355
356            // use index to lookup matching pages
357            $pages = ft_pageLookup($id, true);
358
359            // If 'useheading' option is 'always' or 'content',
360            // search page titles with original query as well.
361            if ($conf['useheading'] == '1' || $conf['useheading'] == 'content') {
362                $pages = array_merge($pages, ft_pageLookup($q, true, true));
363                asort($pages, SORT_STRING);
364            }
365
366            // result contains matches in pages and namespaces
367            // we now extract the matching namespaces to show
368            // them seperately
369            $dirs = [];
370
371            foreach($pages as $pid => $title) {
372                if(strpos(getNS($pid), $id) !== false) {
373                    // match was in the namespace
374                    $dirs[getNS($pid)] = 1; // assoc array avoids dupes
375                } else {
376                    // it is a matching page, add it to the result
377                    $data[] = ['id' => $pid, 'title' => $title, 'type' => 'f'];
378                }
379                unset($pages[$pid]);
380            }
381            foreach(array_keys($dirs) as $dir) {
382                $data[] = ['id' => $dir, 'type' => 'd'];
383            }
384
385        } else {
386
387            $opts = [
388                'depth' => 1,
389                'listfiles' => true,
390                'listdirs' => true,
391                'pagesonly' => true,
392                'firsthead' => true,
393                'sneakyacl' => $conf['sneaky_index']
394            ];
395            if($id) $opts['filematch'] = '^.*\/' . $id;
396            if($id) $opts['dirmatch'] = '^.*\/' . $id;
397            search($data, $conf['datadir'], 'search_universal', $opts, $nsd);
398
399            // add back to upper
400            if($ns) {
401                array_unshift(
402                    $data, ['id' => getNS($ns), 'type' => 'u']
403                );
404            }
405        }
406
407        // fixme sort results in a useful way ?
408
409        if(!count($data)) {
410            echo $lang['nothingfound'];
411            exit;
412        }
413
414        // output the found data
415        $even = 1;
416        foreach($data as $item) {
417            $even *= -1; //zebra
418
419            if(($item['type'] == 'd' || $item['type'] == 'u') && $item['id'] !== '') $item['id'] .= ':';
420            $link = wl($item['id']);
421
422            echo '<div class="' . (($even > 0) ? 'even' : 'odd') . ' type_' . $item['type'] . '">';
423
424            if($item['type'] == 'u') {
425                $name = $lang['upperns'];
426            } else {
427                $name = hsc($item['id']);
428            }
429
430            echo '<a href="' . $link . '" title="' . hsc($item['id']) . '" class="wikilink1">' . $name . '</a>';
431
432            if(!blank($item['title'])) {
433                echo '<span>' . hsc($item['title']) . '</span>';
434            }
435            echo '</div>';
436        }
437
438    }
439}
440