1<?php
2/**
3 * DokuWiki Plugin json (Remote 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
9class remote_plugin_json extends DokuWiki_Remote_Plugin {
10    public function _getMethods() {
11        return [
12            'get' => [
13                'args' => array('string', 'string', 'boolean'),
14                'return' => '[status, JSONdata, log]',
15                'name' => 'get',
16                'doc' => 'Get JSON data from page from data path.'
17            ],
18            'set' => [
19                'args' => array('string', 'string', 'string|array', 'boolean'),
20                'return' => 'OK on success or error description',
21                'name' => 'set',
22                'doc' => 'Set JSON data inside the <json> element inside the wiki page.'
23            ],
24            'append' => [
25                'args' => array('string', 'string', 'string|array'),
26                'return' => 'OK on success or error description',
27                'name' => 'append',
28                'doc' => 'Append JSON data into array inside page inside <json id=xxx> element.'
29            ]
30        ];
31    }
32
33    /**
34     * Generate JSON database on page and return data from the JSON_path.
35     *
36     * @param string    $page_id Absolute id of the wiki page.
37     * @param string    $path    Path on the JSON database.
38     * @param boolean   $addLog  If true, the additional info will be returned too.
39     *
40     * @return array  status => string  'OK' on success or error description
41     *                data => string    JSON data from database
42     *                log => array      Only if $addLog is true. JSON string with
43     *                                  description of database generation.
44     */
45    public function get($page_id, $path = '', $addLog = false) {
46        $json_o = $this->loadHelper('json');
47        $response = ['status' => 'OK'];
48
49        $src = $json_o->parse_src($page_id);
50        if (!is_array($src)) {
51            $response['status'] = sprintf($this->getLang('wrong_pageId'), $page_id);
52        }
53        else if (!page_exists($page_id)) {
54            $response['status'] = sprintf($this->getLang('file_not_found'), $page_id);
55        }
56        //verify file read rights
57        else if(auth_quickaclcheck($page_id) < AUTH_READ) {
58            $response['status'] = sprintf($this->getLang('permision_denied_read'), $page_id);
59        }
60        else {
61            $json = [];
62            $data_parameter = [
63                'json_inline_raw' => '',
64                'src' => $src,
65                'path' => [
66                    'clear' => false,
67                    'array' => false,
68                    'tokens' => []
69                ]
70            ];
71            $log = ['tag' => 'remote', 'path' => $path];
72
73            // generate complete JSON database from wikipage
74            $json_o->add_json($json,
75                              $data_parameter,
76                              $this->getConf('src_recursive'),
77                              $log);
78
79            // get part of JSON data specified by path
80            $response['data'] = $json_o->get($json_o->parse_tokens($path),
81                                             $json);
82
83            // add information about JSON data loading
84            if ($addLog) {
85                $response['log'] = $log;
86            }
87        }
88
89        return $response;
90    }
91
92    /**
93     * Find <json id=… element inside page and set its inline data.
94     *
95     * @param string    $page_id    Absolute id of the wiki page.
96     * @param string    $json_id    'id' attribute of the <json> element on the wiki page.
97     * @param string    $data       JSON data to be put inside <json></json>.
98     * @param boolean   $overwrite  If false, error will be reported if <json> element already contains data.
99     *
100     * @return string   'OK' on success or error description
101     */
102    public function set($page_id, $json_id, $data, $overwrite = false) {
103        $err = '';
104
105        if(!page_exists($page_id)) {
106            $err = sprintf($this->getLang('file_not_found'), $page_id);
107        }
108        //verify file write rights
109        else if(auth_quickaclcheck($page_id) < AUTH_EDIT) {
110            $err = sprintf($this->getLang('permision_denied_write'), $page_id);
111        }
112
113        //verify JSON data (must be an array, empty string or valid JSON string)
114        if($err === '') {
115            if (is_array($data)) {
116                $data = json_encode($data);
117            }
118            else {
119                $json_data = json_decode($data, true);
120                if (!(trim($data) === '' || isset($json_data))) {
121                    $err = $this->getLang('json_error');
122                }
123                unset($json_data);
124            }
125        }
126
127        //verify lock
128        $locked = false;
129        if($err === '') {
130            if(checklock($page_id)) {
131                $err = sprintf($this->getLang('file_locked'), $page_id);
132            }
133            else {
134                lock($page_id);
135                $locked = true;
136            }
137        }
138
139        //read the file
140        if($err === '') {
141            $file = rawWiki($page_id);
142            if(!$file) {
143                $err = sprintf($this->getLang('file_not_found'), $page_id);
144            }
145        }
146
147        //replace json data; must be one match
148        if($err === '') {
149            $file_updated = preg_replace_callback(
150                '/(<(json[a-z0-9]*)\b[^>]*?id\s*=[\s"\']*'.$json_id.'\b.*?>)(.*?)(<\/\2>)/s',
151                function($matches) use(&$err, $data, $overwrite) {
152                    // replace only if forced or empty data
153                    if($overwrite || !trim($matches[3])) {
154                        $replacement = $matches[1].$data.$matches[4];
155                        return $replacement;
156                    }
157                    else {
158                        //set error and keep the original data
159                        $err = 'e';
160                        return $matches[0];
161                    }
162                },
163                $file,
164                -1,
165                $count
166            );
167            if($file_updated) {
168                if($count === 0) {
169                    $err = sprintf($this->getLang('element_not_found'), $json_id);
170                }
171                else if($count !== 1) {
172                    $err = sprintf($this->getLang('duplicated_id'), $json_id);
173                }
174                else if($err === 'e') {
175                    $err = sprintf($this->getLang('not_empty'), $json_id);
176                }
177            }
178            else {
179                $err = sprintf($this->getLang('internal_error'), 'plugin/json/remote/set');
180            }
181        }
182
183        //write file
184        if($err === '') {
185            saveWikiText($page_id, $file_updated, sprintf($this->getLang('json_updated_remote'), $json_id), true);
186            $err = 'OK';
187        }
188
189        //unlock for editing
190        if($locked) {
191            unlock($page_id);
192        }
193
194        return $err;
195    }
196
197
198    /**
199     * Find <json id=… element inside page and append data to its inline database.
200     *
201     * Inline JSON data must be an array or empty. If empty, new array will be initialized.
202     *
203     * @param string    $page_id    Absolute id of the wiki page.
204     * @param string    $json_id    'id' attribute of the <json> element. If empty, complete page will be
205     *                              treated as JSON database (page must be a JSON array, empty or non-existent).
206     * @param string    $data       JSON data to be appended inside <json>[]</json>. Must be an array or valid JSON string.
207     *
208     * @return string   'OK' on success or error description
209     */
210    public function append($page_id, $json_id, $data) {
211        $err = '';
212
213        if(!page_exists($page_id) && $json_id) {
214            $err = sprintf($this->getLang('file_not_found'), $page_id);
215        }
216        //verify file write rights
217        else if(auth_quickaclcheck($page_id) < AUTH_EDIT) {
218            $err = sprintf($this->getLang('permision_denied_write'), $page_id);
219        }
220
221        //verify JSON data (must be an array or valid JSON string)
222        if($err === '') {
223            if (is_array($data)) {
224                $data = json_encode($data);
225            }
226            else {
227                $json_data = json_decode($data, true);
228                if (isset($json_data)) {
229                    $data = json_encode($json_data); //make string in one line
230                }
231                else {
232                    $err = $this->getLang('json_error');
233                }
234                unset($json_data);
235            }
236        }
237
238        //verify lock
239        $locked = false;
240        if($err === '') {
241            if(checklock($page_id)) {
242                $err = sprintf($this->getLang('file_locked'), $page_id);
243            }
244            else {
245                lock($page_id);
246                $locked = true;
247            }
248        }
249
250        //read the file
251        if($err === '') {
252            $file = rawWiki($page_id);
253            if(!$file) {
254                if ($json_id) {
255                    $err = sprintf($this->getLang('file_not_found'), $page_id);
256                }
257                else {
258                    $file = '[]';
259                }
260            }
261        }
262
263        if($err === '') {
264            // pattern "[", "current_data", "]", ignore spaces
265            $json_array_pattern = '/^(\s*\[\s*)(.*?)\s*\]\s*$/s';
266            //a classic dokuwiki page with <json> elements inside
267            if($json_id) {
268                //replace json data; must be one match
269                $file_updated = preg_replace_callback(
270                    '/(<(json[a-z0-9]*)\b[^>]*?id\s*=[\s"\']*'.$json_id.'\b.*?>)(.*?)(<\/\2>)/s',
271                    function($matches) use(&$err, $data, $json_array_pattern) {
272                        $inlineJSON = trim($matches[3]) ? $matches[3] : '[]';
273
274                        if (preg_match($json_array_pattern, $inlineJSON, $matchesJSON)) {
275                            if ($matchesJSON[2]) {
276                                $inlineJSON = $matchesJSON[1].$matchesJSON[2].",\n  ".$data."\n]";
277                            } else {
278                                $inlineJSON = "[\n  ".$data."\n]";
279                            }
280
281                            return $matches[1].$inlineJSON.$matches[4];
282                        }
283                        else {
284                            //set error and keep original data
285                            $err = 'e';
286                            return $matches[0];
287                        }
288                    },
289                    $file,
290                    -1,
291                    $count
292                );
293                if($file_updated) {
294                    if($count === 0) {
295                        $err = sprintf($this->getLang('element_not_found'), $json_id);
296                    }
297                    else if($count !== 1) {
298                        $err = sprintf($this->getLang('duplicated_id'), $json_id);
299                    }
300                    else if($err === 'e') {
301                        $err = sprintf($this->getLang('not_array'), $json_id);
302                    }
303                }
304                else {
305                    $err = sprintf($this->getLang('internal_error'), 'plugin/json/remote/append');
306                }
307            }
308            //a pure JSON file
309            else {
310                if (preg_match($json_array_pattern, $file, $matchesJSON)) {
311                    if ($matchesJSON[2]) {
312                        $file_updated = $matchesJSON[1].$matchesJSON[2].",\n  ".$data."\n]";
313                    } else {
314                        $file_updated = "[\n  ".$data."\n]";
315                    }
316                }
317                else {
318                    $err = sprintf($this->getLang('not_array_file'), $page_id);
319                }
320            }
321        }
322
323        //write file
324        if($err === '') {
325            saveWikiText($page_id, $file_updated, sprintf($this->getLang('json_added_remote'), $json_id), true);
326            $err = 'OK';
327        }
328
329        //unlock for editing
330        if($locked) {
331            unlock($page_id);
332        }
333
334        return $err;
335    }
336}
337