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