1<?php
2/**
3 * DokuWiki Plugin dropfiles (Action Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Michael Große <dokuwiki@cosmocode.de>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) {
11    die();
12}
13
14class action_plugin_dropfiles_ajax extends DokuWiki_Action_Plugin
15{
16
17    protected $NS = '';
18
19
20    /**
21     * Registers a callback function for a given event
22     *
23     * @param Doku_Event_Handler $controller DokuWiki's event controller object
24     *
25     * @return void
26     */
27    public function register(Doku_Event_Handler $controller)
28    {
29        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjaxCallUnknown');
30    }
31
32    /**
33     * [Custom event handler which performs action]
34     *
35     * @param Doku_Event $event event object by reference
36     * @param mixed $param [the parameters passed as fifth argument to register_hook() when this
37     *                           handler was registered]
38     *
39     * @return void
40     */
41
42    public function handleAjaxCallUnknown(Doku_Event $event, $param)
43    {
44        if (strpos($event->data,'dropfiles') !== 0) {
45            return;
46        }
47
48        $event->preventDefault();
49        $event->stopPropagation();
50        $action = substr($event->data, strlen('dropfiles_'));
51
52        if ($action === 'checkfiles') {
53            echo json_encode($this->checkFiles());
54        }
55
56        if ($action === 'mediaupload') {
57            global $INPUT;
58            $this->callMediaupload();
59        }
60    }
61
62    /**
63     * this is an adjusted version of @see ajax_mediaupload
64     *
65     * This version also provides a consistent error key, instead of only giving a localized error message
66     */
67    protected function callMediaupload()
68    {
69        global $NS, $MSG, $INPUT;
70
71        $id = '';
72        if ($_FILES['qqfile']['tmp_name']) {
73            $id = $INPUT->post->str('mediaid', $_FILES['qqfile']['name']);
74        } elseif ($INPUT->get->has('qqfile')) {
75            $id = $INPUT->get->str('qqfile');
76        }
77
78        $id = cleanID($id);
79
80        $NS = $INPUT->str('ns');
81        $ns = $NS . ':' . getNS($id);
82
83        $AUTH = auth_quickaclcheck("$ns:*");
84        if ($AUTH >= AUTH_UPLOAD) {
85            io_createNamespace("$ns:xxx", 'media');
86        }
87
88        if ($_FILES['qqfile']['error']) {
89            unset($_FILES['qqfile']);
90        }
91
92        $res = false;
93        if ($_FILES['qqfile']['tmp_name']) {
94            $res = media_upload($NS, $AUTH, $_FILES['qqfile']);
95        }
96        if ($INPUT->get->has('qqfile')) {
97            $res = media_upload_xhr($NS, $AUTH);
98        }
99
100        if ($res) {
101            $result = array(
102                'success' => true,
103                'link' => media_managerURL(array('ns' => $ns, 'image' => $NS . ':' . $id), '&'),
104                'id' => $NS . ':' . $id,
105                'ns' => $NS,
106            );
107        } else {
108            $error = '';
109            if (count($MSG)) {
110                foreach ($MSG as $msg) {
111                    $error .= $msg['msg'];
112                }
113            }
114            $result = array(
115                'error' => $error,
116                'errorType' => $this->determineErrorCause($id, $ns),
117                'ns' => $NS,
118            );
119        }
120        header('Content-Type: application/json');
121        echo json_encode($result);
122    }
123
124
125    /**
126     * @return array
127     */
128    protected function checkFiles() {
129        global $INPUT;
130        $this->NS = $INPUT->str('ns');
131
132        // loop over files
133        $filelist = $INPUT->post->arr('filenames');
134        return array_reduce($filelist, [$this, 'checkFileCallback'], []);
135    }
136
137    /**
138     * check a list of filenames for existing files and other errors that might prevent upload
139     *
140     * Note: some checks can only be done after the file has actually been uploaded
141     *
142     * @param array $carry
143     * @param string $filename
144     * @return array
145     */
146    protected function checkFileCallback($carry, $filename){
147        $id = cleanID($filename);
148        $ns = $this->NS . ':' . getNS($id);
149        $error = $this->determineErrorCause($id, $ns, false);
150        $carry[$filename] = $error;
151        return $carry;
152    }
153
154
155    /**
156     * Try to determine WHY the upload failed
157     *
158     * This replicates code from several places in dokuwiki core
159     *
160     * @param string $id the name of the new file on the filesystem
161     * @param string $ns the namespace where the new file would have been saved
162     * @param bool $contentAvailable flag if the file hasn't been uploaded yet
163     *
164     * @return string
165     */
166    protected function determineErrorCause($id, $ns, $contentAvailable = true)
167    {
168        global $conf;
169
170        if (!checkSecurityToken()) {
171            return 'security token failed';
172        }
173        $AUTH = auth_quickaclcheck("$ns:*");
174        if ($AUTH < AUTH_UPLOAD) {
175            return 'missing permissions';
176        }
177        $fullID = $ns . ':' . $id;
178        $fn = mediaFN($fullID);
179
180        // get filetype regexp
181        $types = array_keys(getMimeTypes());
182        $types = array_map(
183            function ($q) {
184                return preg_quote($q, '/');
185            },
186            $types
187        );
188        $regex = implode('|', $types);
189        if (!preg_match('/\.(' . $regex . ')$/i', $fn)) {
190            return 'bad file type';
191        }
192
193        $exists = file_exists($fn);
194        if ($exists) {
195            if (!$conf['mediarevisions'] && $AUTH < AUTH_DELETE) {
196                return 'file exists and no delete permissions';
197            }
198
199            return 'file exists';
200        }
201
202        if ($contentAvailable) {
203            list(, $mime) = mimetype($id);
204            $ok = media_contentcheck($fn, $mime);
205            if ($ok === -1) {
206                return 'file does not match extension';
207            }
208            if ($ok === -2) {
209                return 'spam';
210            }
211            if ($ok === -3) {
212                return 'xss';
213            }
214        }
215
216        return '';
217    }
218}
219
220// vim:ts=4:sw=4:et:
221