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