1<?php
2/**
3 * DokuWiki Plugin json (Syntax 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
15class syntax_plugin_json_define extends DokuWiki_Syntax_Plugin
16{
17    /**
18     * @return string Syntax mode type
19     */
20    public function getType() {
21        return 'protected';
22    }
23
24
25    /**
26     * @return string Paragraph type
27     */
28    public function getPType() {
29        return 'block';
30    }
31
32
33    /**
34     * @return int Sort order - Low numbers go before high numbers
35     */
36    public function getSort() {
37        return 185;
38    }
39
40
41    /**
42     * Connect lookup pattern to lexer.
43     *
44     * @param string $mode Parser mode
45     */
46    public function connectTo($mode) {
47        $this->Lexer->addSpecialPattern('<json[a-z0-9]*\b.*?>.*?</json[a-z0-9]*>', $mode, 'plugin_json_define');
48    }
49
50
51    /**
52     * Handle matches of the json syntax
53     *
54     * @param string       $match   The match of the syntax
55     * @param int          $state   The state of the handler
56     * @param int          $pos     The position in the document
57     * @param Doku_Handler $handler The handler
58     *
59     * @return array Data for the renderer
60     */
61    public function handle($match, $state, $pos, Doku_Handler $handler) {
62        $json_o = $this->loadHelper('json');
63        $data = $json_o->handle_element($match);
64
65        if($data === NULL) {
66            return $match;
67        }
68
69        //is there a plugin
70        if(!isset($data['error']) && $data['tag'] !== 'json') {
71            $sub_plugin = $this->loadHelper($data['tag']);
72            if(!($sub_plugin && is_a($sub_plugin, 'helper_plugin_json'))) {
73                unset($sub_plugin);
74                if($this->getConf('ignore_if_no_plugin')) {
75                    return $match;
76                }
77            }
78        }
79
80        //get attribute 'display' with display options, separated by commas
81        if(isset($data['keys']['display'])) {
82            if($data['keys']['display'][0] === ',') {
83                //add options to defaults
84                $data['display'] =
85                    strtolower($this->getConf('json_display')).
86                    strtolower($data['keys']['display']);
87            }
88            else {
89                //use only custom display options
90                $data['display'] = strtolower($data['keys']['display']);
91            }
92        }
93        else {
94            //use default display options
95            $data['display'] = strtolower($this->getConf('json_display'));
96        }
97
98        //Include data, if archive=make
99        if(!isset($data['error']) && isset($data['keys']['archive'])) {
100            if(strtolower($data['keys']['archive']) === 'make') {
101                $data['display'] .= ',orig-hidden';
102            }
103        }
104
105        //call a sub-plugin
106        if(!isset($data['error']) && isset($sub_plugin)) {
107            $data['sub_plugin'] = true;
108            $sub_plugin->handle($data);
109        }
110
111        return $data;
112    }
113
114
115    /**
116     * Render xhtml output or metadata
117     *
118     * @param string        $mode     Renderer mode (supported modes: xhtml)
119     * @param Doku_Renderer $renderer The renderer
120     * @param array         $data     The data from the handler() function
121     *
122     * @return bool If rendering was successful.
123     */
124    public function render($mode, Doku_Renderer $renderer, $data) {
125
126        if($mode === 'metadata') {
127            if(!isset($data['error']) && isset($data['src']['internallink'])) {
128                $renderer->internallink($data['src']['internallink']);
129            }
130        }
131
132        else if($mode === 'xhtml') {
133            $json_o = $this->loadHelper('json');
134            $data_path = isset($data['keys']['path']) ? $data['keys']['path'] : '';
135
136            if(is_string($data)) {
137                $renderer->cdata($data);
138                return true;
139            }
140
141            static $tab_number = 0;
142            $tab_number++;
143
144            $log = array('tag' => $data['tag'], 'id' => $data['id'] ?? '', 'path' => $data_path, 'inline' => (strlen(trim($data['json_inline_raw'])) > 0));
145
146            //buld the json database
147            if(!isset($data['error'])) {
148                if(!isset($data['src_archive'])) {
149                    //check, if src to json file is specified in query string
150                    if(isset($data['keys']['src_ext'])) {
151                        $src = NULL;
152                        $src_ext = strtolower($data['keys']['src_ext']);
153                        if(preg_match('/^json_\w+$/', $src_ext)) {
154                            //scan query string for matching key
155                            foreach ($_GET as $q_key => $q_val) {
156                                if(strtolower($q_key) == $src_ext) {
157                                    $src = $json_o->parse_src($q_val);
158                                    break;
159                                }
160                            }
161                        }
162                        if(is_string($src)) {
163                            $data['src'] = $src;
164                            $data['src_extractors'] = $json_o->extractors_handle($src);
165                        }
166                        else if(is_array($src)) {
167                            $data['src'] = $src;
168                        }
169                        else if(!isset($data['src'])) {
170                            $log['error'] = 'query string for src_ext='.$data['keys']['src_ext'].' not defined';
171                        }
172                    }
173
174                    //check, if src_path to json file is specified in query string
175                    if(isset($data['keys']['src_path_ext'])) {
176                        $src_path = NULL;
177                        $src_path_ext = strtolower($data['keys']['src_path_ext']);
178                        if(preg_match('/^json_\w+$/', $src_path_ext)) {
179                            //scan query string for matching key
180                            foreach ($_GET as $q_key => $q_val) {
181                                if(strtolower($q_key) == $src_path_ext) {
182                                    $src_path = $json_o->parse_tokens($q_val);
183                                    break;
184                                }
185                            }
186                        }
187                        if(is_array($src_path)) {
188                            $data['src_path'] = $src_path;
189                        }
190                        else if(!is_array($data['src_path'])) {
191                            $log['error'] = 'query string for src_path_ext='.$data['keys']['src_path_ext'].' not defined';
192                        }
193                    }
194
195                    //disable browser cache, if external files are used for data
196                    if(isset($data['src']) && is_array($data['src'])) {
197                        $renderer->nocache();
198                    }
199                }
200
201                //load all json data and put it into the json database
202                $json_o->add_json(helper_plugin_json::$json, $data, $this->getConf('src_recursive'), $log);
203            }
204            else {
205                $log['error'] = $data['error'];
206            }
207
208
209            //prapare data for html output (jQuery UI tabs)
210            $class = array('json-tabs');
211            if(isset($data['make_archive'])) {
212                $class[] = 'json-make-archive';
213            }
214            $data_attr = array(
215                'json-id' => $data['id'] ?? '',
216                'json-hash' => md5($data['json_inline_raw']),
217                'active' => 'false');   //all tabs colapsed or specific tab active
218            $tabs = array();
219            $body = array();
220            $display = $data['display'];
221            $all = strpos($display, 'all') !== false;
222            $tab_no = 0;
223
224            //json original data (before they are combined with inline data)
225            if($all || strpos($display, 'original') !== false) {
226                if(strpos($display, 'original*') !== false) { $data_attr['active'] = $tab_no; }
227                $tab_no++;
228                $tabs[] = '<li><a href="#json-tab-'.$tab_number.'-orig">'.$this->getLang('json_original').'</a></li>';
229                $body[] =      '<div id="json-tab-'.$tab_number.'-orig"><pre class="json-data-original lang-json">'
230                        .htmlspecialchars(json_encode($data['json_original'], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)).'</pre></div>';
231            }
232            else if(strpos($display, 'orig-hidden') !== false) {
233                $body[] = '<div hidden=""><pre class="json-data-original">'
234                        .htmlspecialchars(json_encode($data['json_original'])).'</pre></div>';
235            }
236
237            //json inline data
238            if($all || strpos($display, 'inline') !== false) {
239                if(strpos($display, 'inline*') !== false) { $data_attr['active'] = $tab_no; }
240                $tab_no++;
241                $tabs[] = '<li><a href="#json-tab-'.$tab_number.'-inline">'.$this->getLang('json_inline').'</a></li>';
242                $body[] =      '<div id="json-tab-'.$tab_number.'-inline"><textarea wrap="off" class="json-data-inline json-textarea">'
243                        .htmlspecialchars($data['json_inline_raw']).'</textarea></div>';
244            }
245            else if(strpos($display, 'inl-hidden') !== false) {
246                $body[] = '<div hidden=""><textarea class="json-data-inline">'
247                        .htmlspecialchars($data['json_inline_raw']).'</textarea></div>';
248            }
249
250            //json combined data
251            if($all || strpos($display, 'combined') !== false) {
252                if(strpos($display, 'combined*') !== false || ($all && $data_attr['active'] === 'false')) { $data_attr['active'] = $tab_no; }
253                $tab_no++;
254                $tabs[] = '<li><a href="#json-tab-'.$tab_number.'-comb">'.$this->getLang('json_combined').'</a></li>';
255                $body[] =      '<div id="json-tab-'.$tab_number.'-comb"><pre class="json-data-combined lang-json">'
256                        .htmlspecialchars(json_encode($data['json_combined'], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)).'</pre></div>';
257            }
258            else if(strpos($display, 'comb-hidden') !== false) {
259                $body[] = '<div hidden=""><pre class="json-data-combined">'
260                        .htmlspecialchars(json_encode($data['json_combined'])).'</pre></div>';
261            }
262
263            //call a sub-plugin
264            if(isset($data['sub_plugin'])) {
265                $sub_plugin = $this->loadHelper($data['tag']);
266                $sub_plugin->render($renderer, $data, $class, $data_attr, $tabs, $body, $log, $tab_no, $tab_number);
267            }
268
269            //display 'error' log when there are errors or display 'log', when there are external source files.
270            if(($all || (strpos($display, 'error') !== false) || (strpos($display, 'log') !== false)) && $this->find_key('error', $log)) {
271                if(strpos($display, 'error*') !== false || strpos($display, 'log*') !== false) { $data_attr['active'] = $tab_no; }
272                $tab_no++;
273                $tabs[] = '<li><a href="#json-tab-'.$tab_number.'-err" class="json-error">'.$this->getLang('error').'</a></li>';
274                $body[] = '<div id="json-tab-'     .$tab_number.'-err" class="json-log">'.$this->render_log($renderer, array($log)).'</div>';
275            }
276            else if($all || (strpos($display, 'log') !== false)) {
277                if(strpos($display, 'log*') !== false) { $data_attr['active'] = $tab_no; }
278                $tab_no++;
279                $tabs[] = '<li><a href="#json-tab-'.$tab_number.'-log">'.$this->getLang('log').'</a></li>';
280                $body[] =      '<div id="json-tab-'.$tab_number.'-log" class="json-log">'.$this->render_log($renderer, array($log)).'</div>';
281            }
282
283            //no tabs, completelly hide the element
284            if($tab_no === 0) {
285                $class[] = 'json-hidden';
286            }
287            //if single tab is there and is set to default, then hide tabs menu
288            else if($tab_no === 1 && $data_attr['active'] !== 'false') {
289                $class[] = 'json-hide-tabs';
290            }
291
292            //write html
293            if(count($body) > 0) {
294                $renderer->doc .= '<div class="'.implode(' ', $class).'" '.$this->implode_data_attr($data_attr).'>'
295                                ."\n<ul>\n"
296                                ."  <li><a>".$data_path."</a> &nbsp; <button class='json-save-button'>".$this->getLang('save')."</button></li>\n  "
297                                .implode("\n  ", $tabs)
298                                ."\n</ul>\n"
299                                .implode("\n", $body)
300                                ."\n</div>";
301            }
302        }
303        return true;
304    }
305
306
307    /**
308     * Verify, if key exists in multidimensional array
309     */
310    private function find_key($keySearch, $array) {
311        foreach($array as $key => $item) {
312            if($key === $keySearch) {
313                return true;
314            } elseif (is_array($item) && $this->find_key($keySearch, $item)) {
315                return true;
316            }
317        }
318        return false;
319    }
320
321
322    /**
323     * return string with html data-... attributes
324     */
325    private function implode_data_attr($arr) {
326        $s = [];
327        foreach($arr as $key => $val) {
328            $s[] = 'data-'.$key.'="'.$val.'"';
329        }
330        return implode(' ', $s);
331    }
332
333
334    /**
335     * Render json log
336     *
337     * @param Doku_Renderer $r The renderer
338     * @param array $log Log data elements from <json> element
339     * @param integer $level list level
340     *
341     * @return string html list with info about JSON data source
342     */
343    private function render_log(Doku_Renderer $renderer, $log_elements, $level=1) {
344        $doc = '<ul>'.DOKU_LF;
345
346        foreach($log_elements as $el) {
347
348            //listitem with info about <json> element
349            $doc .= '<li class="level'.$level.'"><div class="li">';
350            $doc .= 'element: '.$el['tag'];
351            if ($el['id']) $doc .= ', id: '.$el['id'];
352            $doc .= ', path: '.htmlspecialchars($el['path']);
353            if(isset($el['error'])) {
354                $doc .= ', <span class="json-error">ERROR</span>: '.htmlspecialchars($el['error']);
355            }
356            if(!empty($el['src_archive'])) {
357                $doc .= ', archived src data';
358            }
359            if($el['inline']) {
360                $doc .= ', inline data';
361            }
362            if(isset($el['src'])) {
363                $doc .= ', external data';
364                if(isset($el['src_path'])) {
365                    $doc .= ' (from path: '.htmlspecialchars($el['src_path']).')';
366                }
367            }
368            $doc .= '</div></li>'.DOKU_LF;
369
370            //list of files with external json data
371            if(isset($el['src'])) {
372                $doc .= '<ul>'.DOKU_LF;
373                foreach($el['src'] as $file) {
374                    $doc .= '<li class="level'.($level+1).'"><div class="li">';
375                    if($file['filename'] === '***JSON code***') {
376                        $doc .= 'JSON code from \'src\' attribute';
377                    }
378                    else if(isset($file['extenal_link'])) {
379                        $doc .= 'external file: '.$renderer->externallink($file['filename'], $file['filename'], true);
380                    }
381                    else {
382                        $doc .= 'internal file: '.$renderer->internallink($file['filename'], $file['filename'], null, true);
383                    }
384                    if(isset($file['error'])) {
385                        $doc .= ', <span class="json-error">ERROR</span>: '.htmlspecialchars($file['error']);
386                    }
387                    else if(!isset($file['elements']) && $file['filename'] !== '***JSON code***') {
388                        $doc .= ', JSON file';
389                    }
390                    $doc .= '</div></li>'.DOKU_LF;
391                    if(isset($file['elements'])) {
392                        $doc .= $this->render_log($renderer, $file['elements'], $level+2);
393                    }
394                }
395                $doc .= '</ul>'.DOKU_LF;
396            }
397        }
398
399        $doc .= '</ul>'.DOKU_LF;
400
401        return $doc;
402    }
403
404}
405