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