1<?php
2/**
3 * DokuWiki Plugin json (Action Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Janez Paternoster <janez.paternoster@siol.net>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) {
11    die();
12}
13
14//Resolve to absolute page ID
15use dokuwiki\File\PageResolver;
16
17class action_plugin_json extends DokuWiki_Action_Plugin
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        $controller->register_hook('DOKUWIKI_STARTED', 'AFTER',  $this, 'handle_jsinfo');
29        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handle_ajax_call_unknown');
30    }
31
32
33    /**
34     * Handle event DOKUWIKI_STARTED - add data to JSINFO
35     *
36     * @param Doku_Event $event  event object by reference
37     * @param mixed      $param  [the parameters passed as fifth argument to register_hook() when this
38     *                           handler was registered]
39     */
40    public function handle_jsinfo(&$event, $param) {
41        global $JSINFO;
42        global $ID;
43
44        if(page_exists($ID)) {
45            $JSINFO['json_lastmod'] = filemtime(wikiFN($ID));
46            $JSINFO['enable_ejs'] = $this->getConf('enable_ejs');
47        }
48    }
49
50
51    /**
52     * Handle event AJAX_CALL_UNKNOWN for json_plugin_save_inline and
53     * json_plugin_archive call
54     *
55     * @param Doku_Event $event  event object by reference
56     * @param mixed      $param  [the parameters passed as fifth argument to register_hook() when this
57     *                           handler was registered]
58     *
59     * @return Ajax response as json
60     */
61    public function handle_ajax_call_unknown(Doku_Event $event, $param) {
62        if ($event->data === 'json_plugin_save_inline') {
63            $this->ajax_save_inline($event, $param);
64        }
65        else if ($event->data === 'json_plugin_archive') {
66            $this->ajax_archive($event, $param);
67        }
68    }
69
70    private function ajax_save_inline(Doku_Event $event, $param) {
71        //no other ajax call handlers needed
72        $event->stopPropagation();
73        $event->preventDefault();
74
75        //access additional request variables
76        global $INPUT;
77        $page_to_modify = $INPUT->str('file');
78        $json_id = $INPUT->str('id');
79        $hash_received = $INPUT->str('hash');
80        $text_received = $INPUT->str('text');
81
82        $resolver = new PageResolver('');
83        $page_to_modify = $resolver->resolveId($page_to_modify);
84
85        $err = '';
86        $hash_new = '';
87
88        if(!page_exists($page_to_modify)) {
89            $err = sprintf($this->getLang('file_not_found'), $page_to_modify);
90        }
91        //verify file write rights
92        else if(auth_quickaclcheck($page_to_modify) < AUTH_EDIT) {
93            $err = sprintf($this->getLang('permision_denied_write'), $page_to_modify);
94        }
95        //verify id
96        else if(!$json_id) {
97            $err = sprintf($this->getLang('missing_id'), $page_to_modify);
98        }
99
100        //verify lock
101        $locked = false;
102        if($err === '') {
103            if(checklock($page_to_modify)) {
104                $err = sprintf($this->getLang('file_locked'), $page_to_modify);
105            }
106            else {
107                lock($page_to_modify);
108                $locked = true;
109            }
110        }
111
112        //read the file
113        if($err === '') {
114            $file = rawWiki($page_to_modify);
115            if(!$file) {
116                $err = sprintf($this->getLang('file_not_found'), $page_to_modify);
117            }
118        }
119
120        //replace json data; must be one match
121        if($err === '') {
122            $file_updated = preg_replace_callback(
123                '/(<(json[a-z0-9]*)\b[^>]*?id\s*=[\s"\']*'.$json_id.'\b.*?>)(.*?)(<\/\2>)/s',
124                function($matches) use(&$err, $hash_received, $text_received, &$hash_new) {
125                    //Make sure, no one changed the original data. Ajax call must know the md5 hash
126                    //of the actual data saved inside the file. If someone changed the data between
127                    //page reload (or previous ajax call) and last ajax call, then last ajax call
128                    //will fail with error message.
129                    $hash_original = md5($matches[3]);
130                    if($hash_original === $hash_received) {
131                        $replacement = $matches[1].$text_received.$matches[4];
132                        $hash_new = md5($text_received);
133                        return $replacement;
134                    }
135                    else {
136                        //set error and keep original data
137                        $err = 'e';
138                        return $matches[0];
139                    }
140                },
141                $file,
142                -1,
143                $count
144            );
145            if($file_updated) {
146                if($count === 0) {
147                    $err = sprintf($this->getLang('element_not_found'), $json_id);
148                }
149                else if($count !== 1) {
150                    $err = sprintf($this->getLang('duplicated_id'), $json_id);
151                }
152                else if($err === 'e') {
153                    $err = sprintf($this->getLang('hash_not_equal'), $json_id);
154                }
155            }
156            else {
157                $err = sprintf($this->getLang('internal_error'), 'plugin/json/action');
158            }
159        }
160
161        //write file
162        if($err === '') {
163            saveWikiText($page_to_modify, $file_updated, sprintf($this->getLang('json_updated_ajax'), $json_id), true);
164            $response = array('response' => 'OK', 'hash' => $hash_new);
165        }
166        else {
167            $response = array('response' => 'error', 'error' => $err);
168        }
169
170        //unlock for editing
171        if($locked === true) {
172            unlock($page_to_modify);
173        }
174
175        //send response
176        header('Content-Type: application/json');
177        echo json_encode($response);
178    }
179
180    private function ajax_archive(Doku_Event $event, $param) {
181        //no other ajax call handlers needed
182        $event->stopPropagation();
183        $event->preventDefault();
184
185        //access additional request variables
186        global $INPUT;
187        $page_to_modify = $INPUT->str('file');
188        $resolver = new PageResolver('');
189        $page_to_modify = $resolver->resolveId($page_to_modify);
190
191        $lastmod_current = filemtime(wikiFN($page_to_modify));
192        $lastmod_call = $INPUT->str('lastmod');
193        $data_original = $INPUT->arr('data');
194        $subdir = $INPUT->str('subdir');
195        $err = '';
196
197        if(!page_exists($page_to_modify)) {
198            $err = sprintf($this->getLang('file_not_found'), $page_to_modify);
199        }
200        //verify file write rights
201        else if(auth_quickaclcheck($page_to_modify) < AUTH_EDIT) {
202            $err = sprintf($this->getLang('permision_denied_write'), $page_to_modify);
203        }
204        //verify if page was not modified
205        else if($lastmod_call != $lastmod_current) {
206            $err = sprintf($this->getLang('file_changed'), $page_to_modify);
207        }
208
209        //verify lock
210        $locked = false;
211        if($err === '') {
212            if(checklock($page_to_modify)) {
213                $err = sprintf($this->getLang('file_locked'), $page_to_modify);
214            }
215            else {
216                lock($page_to_modify);
217                $locked = true;
218            }
219        }
220
221        //read the file
222        if($err === '') {
223            $file = rawWiki($page_to_modify);
224            if(!$file) {
225                $err = sprintf($this->getLang('file_not_found'), $page_to_modify);
226            }
227        }
228
229        //replace json data
230        if($err === '') {
231            $counter = 0;
232            $file_updated = preg_replace_callback(
233                '/(<(json[a-z0-9]*)\b[^>]*?)(archive\s*=\s*(["\']?)(make|disable)\4)(.*?>)(.*?<\/\2>)/is',
234                function($matches) use($data_original, &$counter) {
235                    //el_1='<jsonxx ...', el_2='achive=make', el_3=' ...>', rest='... </jsonxxx>'
236                    list(, $el_1, ,$el_2, ,$make_disable, $el_3, $rest) = $matches;
237                    if($make_disable === 'make') {
238                        $data = $data_original[$counter];
239                        $data = str_replace('\'', '&#39;', $data);
240                        $data = str_replace('<', '&lt;', $data);
241                        $data = str_replace('>', '&gt;', $data);
242                        $element =  $el_1."archive=\n'$data'\n".$el_3;
243                    }
244                    else {
245                        $element = $el_1.'archive_disabled=disable'.$el_3;
246                        $element = preg_replace('/\bsrc\s*=/', 'src_disabled=', $element);
247                        $element = preg_replace('/\bsrc_ext\s*=/', 'src_ext_disabled=', $element);
248                    }
249                    $counter++;
250                    return $element.$rest;
251                },
252                $file
253            );
254            if($counter === 0 || $counter !== count($data_original)) {
255                $err = 'Internal error - number of <json archive=make ...>('.$counter
256                    .') is zero or not equal to number of requests('.count($data_original).')';
257            }
258        }
259
260        if($err === '') {
261            //move the file to the sub directory
262            if ($subdir) {
263                if (str_contains($page_to_modify, ':')) {
264                    $newId = preg_replace('/^(.*:)([^:]+)$/',
265                                          '${1}'.$subdir.':${2}',
266                                          $page_to_modify);
267                }
268                else {
269                    $newId = $subdir.':'.$page_to_modify;
270                }
271
272                if(page_exists($newId)) {
273                    $err = sprintf($this->getLang('file_exists'), $newId);
274                }
275                //verify file write rights
276                else if(auth_quickaclcheck($newId) < AUTH_EDIT) {
277                    $err = sprintf($this->getLang('permision_denied_write'), $page_to_modify);
278                }
279                //save to the new location and delete the old file
280                else {
281                    saveWikiText($newId, $file_updated, $this->getLang('archived'));
282                    if(page_exists($newId)) {
283                        saveWikiText($page_to_modify, null, $this->getLang('archived'));
284                    }
285                }
286            }
287            //only write the file to the current location
288            else {
289                saveWikiText($page_to_modify, $file_updated, $this->getLang('archived'));
290            }
291        }
292
293        if($err === '') {
294            $response = array('response' => 'OK');
295        }
296        else {
297            $response = array('response' => 'error', 'error' => $err);
298        }
299
300        //unlock for editing
301        if($locked === true) {
302            unlock($page_to_modify);
303        }
304
305        //send response
306        header('Content-Type: application/json');
307        echo json_encode($response);
308    }
309}
310