1<?php
2/**
3 * DokuWiki Plugin json (Helper 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
15/**
16 * Resolving relative IDs to absolute ones
17 *
18 * Class is necessary, because dokuwiki\File\PageResolver is not able not to cleanID().
19 *
20 * see https://www.dokuwiki.org/devel:releases:refactor2021#refactoring_of_id_resolving
21 */
22class CustomResolver extends dokuwiki\File\Resolver
23{
24    /**
25     * Resolves a given ID to be absolute
26     */
27    public function resolveId($id, $rev = '', $isDateAt = false)
28    {
29        $id = (string) $id;
30
31        if ($id !== '') {
32            $id = parent::resolveId($id, $rev, $isDateAt);
33        } else {
34            $id = $this->contextID;
35        }
36
37        return $id;
38    }
39}
40
41
42class helper_plugin_json extends DokuWiki_Plugin {
43    /**
44     * Array with json data objects
45     */
46    public static $json = null;
47
48
49    /**
50     * Array with replacement macros for preprocess
51     */
52    private static $macros = null;
53
54
55    /**
56     * Preprocess matched string
57     *
58     * If textinsert Plugin is installed, use it's macros. (see https://www.dokuwiki.org/plugin:textinsert)
59     * Replace #@macro_name@# patterns with strings defined by textinsert Plugin.
60     *
61     * @param string $match input string
62     *
63     * @return string
64     */
65    public function preprocess($str) {
66        $macros = &helper_plugin_json::$macros;
67
68        //first time load macros or set it to false
69        if(is_null($macros)) {
70            $macros_file = DOKU_INC . 'data/meta/macros/macros.ser';
71            if(file_exists($macros_file)) {
72                $macros = unserialize(file_get_contents($macros_file));
73            }
74            else {
75                $macros = false;
76            }
77        }
78
79        //replace macros
80        if($macros !== false && is_string($str)) {
81            $str = preg_replace_callback(
82                '/#@(.+?)@#/',
83                function ($matches) use ($macros_file, &$macros) {
84                    $replacement = $macros[$matches[1]];
85                    return is_string($replacement) ? $replacement : $matches[0];
86                },
87                $str
88            );
89        }
90
91        return $str;
92    }
93
94
95    /**
96     * Evaluate src attribute (filename and full path or JSON string)
97     *
98     * @param string $path local filename or url
99     *
100     * @return false on error or
101     *         string with json data or
102     *         array['link' => absolute path to file or multiple files, if wildcards are used,
103     *               'fragment' => fragment part of the url (to match specific ID on the page)
104     *               'serverpath' => set to server path if exsits,
105     *               'internal_link' => set if dokuwiki internal link]
106     */
107    public function parse_src($src) {
108        $ret = false;
109        $src = strtolower($src);
110
111        if(preg_match('/^\s*[%\{\[].*[%\}\]]\s*$/s', $src)) {
112            //json object{...} or array[...] or extractor %$...%
113            $ret = $src;
114        }
115        else if(preg_match('#^([a-z0-9\-\.+]+?)://#i', $src)) {
116            //external link (accepts all protocols)
117            $ret = array('link' => $src);
118        }
119        else if(preg_match('/^[\w\/:.#\!\*\?\[\^\-\]\{\,\}]+$/', $src)) {
120            global $conf;
121
122            //DokuWiki internal link with optional wildcards
123            list($base, $fragment) = array_pad(explode('#', $src), 2, '');
124
125            //Resolve to absolute page ID
126            $resolver = new CustomResolver(getID());
127            $base = $resolver->resolveId($base);
128
129            if(strlen($base) > 0) {
130                $ret = array('serverpath' => $conf['datadir']);
131
132                if($base === cleanID($base)) {
133                    $ret['internal_link'] = $base;
134                }
135                if($base[0] !== ':') {
136                    $base = ':'.$base;
137                }
138                $base = str_replace(':', '/', $base);
139
140                $ret['link'] = $ret['serverpath'].$base.'.txt';
141                $ret['fragment'] = $fragment;
142            }
143        }
144
145        return $ret;
146    }
147
148
149    /**
150     * Parse a string into key - value pairs.
151     *
152     * Single or double quotes can be used for key or value. Single quotes
153     * can be nested inside double and vice versa. Empty value can be set
154     * with empty quotes.
155     * Spaces, newlines, etc. are completely ignored, except inside quotes.
156     * There can be multiple quotes for one argument, for example
157     * key = car.'Alfa Romeo'
158     *          .'155 GTA'
159     * will give: ["key"] => "car.Alfa Romeo.155 GTA"
160     *
161     * @param string $str string to parse
162     * @param string $key_delim_val delimiter character between the key and value, space is not allowed
163     * @param string $key_val_delim delimiter character after the key and value pair, space is allowed
164     *
165     * @return array with all options or integer with location of error
166     */
167    public function parse_key_val($str, $key_delim_val = '=', $key_val_delim = ' ') {
168        $options = array();     //return value
169        $quote = false;         //is inside quote " or '
170        $quote_closed = false;  //quote was closed - in use when we want to pass empty value ""
171        $key = $val = '';       //contents of current key and value
172        $arg = &$key;           //reference to key or to value
173        $arg_is_key = true;     //to which is arg the reference
174        $error = false;         //error indication
175
176        $len = strlen($str);
177        for($i = 0; $i < $len; $i++) {
178            $c = $str[$i];
179            if(!$quote && ($c === '"' || $c === '\'')) {
180                $quote = $c;
181            }
182            else if($c === $quote) {
183                $quote = false;
184                $quote_closed = true;
185            }
186            else if($quote !== false) {
187                //inside quote add all
188                $arg .= $c;
189            }
190            else if($c === $key_delim_val) {
191                if($arg_is_key && strlen($key) > 0) {
192                    $arg = &$val;
193                    $arg_is_key = false;
194                    $quote_closed = false;
195                }
196                else {
197                    $error = true;
198                    break;
199                }
200            }
201            else if($c === $key_val_delim) {
202                if(!$arg_is_key && (strlen($val) > 0 || $quote_closed)) {
203                    $options[$key] = $val;
204                    $key = $val = '';
205                    $arg = &$key;
206                    $arg_is_key = true;
207                    $quote_closed = false;
208                }
209                else if(!ctype_space($c)) {
210                    $error = true;
211                    break;
212                }
213            }
214            else if(!ctype_space($c)) { //just ignore spaces which are not inside quotes or are delimitters
215                $arg .= $c;
216            }
217        }
218
219        //write last pair
220        if(!$error && strlen($key) > 0) {
221            if(!$arg_is_key && (strlen($val) > 0 || $quote_closed)) {
222                $options[$key] = $val;
223            }
224            else {
225                $error = true;
226            }
227        }
228
229        return $error ? $i : $options;
230    }
231
232
233    /**
234     * Parse tokens from string (variable chain)
235     *
236     * @param string $str "per. pets. 0 . main color"
237     *
238     * @return array of tokens: ["per", "pets", "0", "main color"]
239     */
240    public function parse_tokens($str) {
241        if(is_string($str)) {
242            //remove extra spaces
243            $str = trim(preg_replace('/\s*\.\s*/', '.', $str), ". \t\n\r\0\x0B");
244            return $str==="" ? array() : explode('.', $str);
245        }
246        else {
247            return array();
248        }
249    }
250
251
252    /**
253     * Parse key=>tokenized_link pairs.
254     *
255     * @param string $str '"Name":name, "Main color" : color .main'
256     *
257     * @return array of tokenized links or "" on empty string or false on syntax error
258     *          ["Name" => [name], "Main color" => [color, main]]
259     */
260    public function parse_links($str) {
261        $str = trim($str);
262        if($str) {
263            $pairs = helper_plugin_json::parse_key_val($str, ':', ',');
264            if(is_array($pairs)) {
265                foreach ($pairs as &$val) {
266                    $val = helper_plugin_json::parse_tokens($val);
267                }
268                return $pairs;
269            }
270            else {
271                return false;
272            }
273        }
274        else {
275            return "";
276        }
277    }
278
279
280    /**
281     * Parse filter expression for table
282     *
283     * @param string $str 'path.to.var >= some_value and path.2 < other_value'
284     *
285     * @return array of logical expressions object:
286     *          ["and" => boolean, "tokens" => array, "operator" => string, "value" => string]
287     */
288    public function parse_filter($str) {
289        $filter = array();
290        if($str) {
291            $exp = preg_split('/\b(and|or)\b/i' , $str , -1 , PREG_SPLIT_DELIM_CAPTURE);
292            array_unshift($exp, 'or');
293
294            while(count($exp) >= 2) {
295                $and = (strtolower(array_shift($exp)) === 'and') ? true : false;
296                list($tokens, $op, $val) = array_pad(preg_split('/(<=|>=|!=|=+|<|>)/',
297                                array_shift($exp), -1 , PREG_SPLIT_DELIM_CAPTURE), 3, '');
298                if($op === '') {
299                    $op = '!=';
300                }
301                else if ($op[0] === '=') {
302                    //operators '=', '==' and '===' are the same.
303                    $op = '==';
304                }
305
306                $filter[] = array(
307                    'and' => $and,
308                    'tokens' => helper_plugin_json::parse_tokens($tokens),
309                    'operator' => $op,
310                    'value' => strtolower(trim($val))
311                );
312            }
313        }
314        return $filter;
315    }
316
317
318    /**
319     * Verify filter for variable
320     *
321     * @param array $var json variable
322     * @param array $filter as returned from parse_filter
323     *
324     * @return boolean true, if variable matches the filter
325     */
326    public function filter($var, $filter) {
327        $match = false;
328        foreach($filter as $f) {
329            $v = $var;
330            foreach($f['tokens'] as $tok) {
331                if(is_array($v) && isset($v[$tok])) {
332                    $v = $v[$tok];
333                }
334                else {
335                    $v = null;
336                    break;
337                }
338            }
339            //case insensitive comparission of strings
340            if(is_string($v)) {
341                $v = strtolower($v);
342            }
343            switch($f['operator']) {
344                case '==': $comp = ($v == $f['value']); break;
345                case '!=': $comp = ($v != $f['value']); break;
346                case '<':  $comp = ($v <  $f['value']); break;
347                case '>':  $comp = ($v >  $f['value']); break;
348                case '<=': $comp = ($v <= $f['value']); break;
349                case '>=': $comp = ($v >= $f['value']); break;
350                default:   $comp = false; break;
351            }
352            $match = $f['and'] ? ($match && $comp) : ($match || $comp);
353        }
354        return $match;
355    }
356
357
358    /**
359     * Handle extractors inside string
360     *
361     * @param string $str input string, which contains '%$path[(row_filter){row_inserts}](filter)%' elements inside
362     *
363     * @return array of extractor data:
364     *          ["tokens" => array, "row_filter" => array, "row_inserts" => array, "filter" => array]
365     */
366    private static $extractor_reg = '/%\$([^[\]\(\)]*?)(?:\[(?:\((.*?)\))?(?:\{(.*?)\})?\])?\s*(?:\((.*?)\))?\s*%/s';
367    public function extractors_handle($str) {
368        $extractors = array();
369
370        preg_match_all(helper_plugin_json::$extractor_reg, $str, $matches_all, PREG_SET_ORDER);
371        foreach ($matches_all as $matches) {
372            list(, $tokens, $row_filter, $row_inserts, $filter) = array_pad($matches, 5, '');
373
374            //handle row inserts: {property_path: json_path_1.{reference_path}.json_path_2, ... }
375            $row_inserts_array = array();
376            $row_inserts = trim($row_inserts);
377            if($row_inserts) {
378                $row_inserts = helper_plugin_json::parse_key_val($row_inserts, ':', ',');
379                if(is_array($row_inserts)) {
380                    foreach ($row_inserts as $key => $val) {
381                        if(preg_match('/^(.*?)\.\{(.*?)\}(?:\.(.*))?$/', $val, $matches_insert)) {
382                            $row_inserts_array[] = array(
383                                'property_path'  => helper_plugin_json::parse_tokens(strval($key)),
384                                'json_path_1'    => helper_plugin_json::parse_tokens($matches_insert[1]),
385                                'reference_path' => helper_plugin_json::parse_tokens($matches_insert[2]),
386                                'json_path_2'    => helper_plugin_json::parse_tokens($matches_insert[3])
387                            );
388                        }
389                    }
390                }
391            }
392
393            $extractors[] = array(
394                'tokens' => helper_plugin_json::parse_tokens($tokens),
395                'row_filter' => helper_plugin_json::parse_filter($row_filter),
396                'row_inserts' => $row_inserts_array,
397                'filter' => helper_plugin_json::parse_filter($filter)
398            );
399        }
400        return $extractors;
401    }
402
403
404    /**
405     * Replace extractors inside string
406     *
407     * @param string $str input string, which contains '%$ ... %' elements inside
408     * @param array $extractors as returned from extractors_handle
409     * @param array $json_database If specified, it will be used for source data.
410     *              Othervise current JSON database will be used.
411     *
412     * @return string with extractors replaced by variables from json database.
413     */
414    public function extractors_replace($str, $extractors, $json_database = NULL) {
415        $replaced = preg_replace_callback(
416            helper_plugin_json::$extractor_reg,
417            function ($matches) use (&$extractors, $json_database) {
418                $result = '';
419                $ext = array_shift($extractors);
420                $json_var = helper_plugin_json::get($ext['tokens'], $json_database);
421                if(isset($json_var)) {
422                    if(count($ext['filter']) > 0 && !helper_plugin_json::filter($json_var, $ext['filter'])) {
423                        unset($json_var);
424                    }
425                    else if (count($ext['row_filter']) > 0) {
426                        //remove all elements from $json_var array, which do not match filter
427                        if(is_array($json_var)) {
428                            //are keys in sequence: 0, 1, 2, ...
429                            $i = 0;
430                            $indexed = true;
431                            foreach($json_var as $key => $val) {
432                                if($key !== $i++) {
433                                    $indexed = false;
434                                    break;
435                                }
436                            }
437                            //filter out rows
438                            $json_var = array_filter($json_var, function ($var) use ($ext) {
439                                return helper_plugin_json::filter($var, $ext['row_filter']);
440                            });
441                            //re-index array
442                            if($indexed) {
443                                $json_var = array_values($json_var);
444                            }
445                        }
446                        else {
447                            unset($json_var);
448                        }
449                    }
450                }
451                //add row inserts
452                if(is_array($json_var)) foreach($json_var as &$item) {
453                    if(is_array($item)) foreach($ext['row_inserts'] as $row_insert) { //if $item is not array, it's no sense to add additional property
454                        //get reference string from specified $item property
455                        if(count($row_insert['reference_path']) === 0) {
456                            $reference = [];
457                        }
458                        else {
459                            $reference = helper_plugin_json::get($row_insert['reference_path'], $item);
460                            if(is_string($reference)) {
461                                $reference = array($reference);
462                            }
463                            else {
464                                continue;
465                            }
466                        }
467                        //get referenced path and value
468                        $val_path = array_merge($row_insert['json_path_1'], $reference, $row_insert['json_path_2']);
469                        $val = helper_plugin_json::get($val_path, $json_database);
470
471                        //add new property to specified item path
472                        $item_copy = &$item;
473                        foreach($row_insert['property_path'] as $tok) {
474                            if(!is_array($item_copy)) {
475                                $item_copy = array();
476                            }
477                            $item_copy[$tok] = null;
478                            $item_copy = &$item_copy[$tok];
479                        }
480                        $item_copy = $val;
481                        unset($item_copy);
482                    }
483                }
484
485                return is_string($json_var) ? $json_var : json_encode($json_var);
486            },
487            $str
488        );
489
490        return $replaced;
491    }
492
493
494    /**
495     * Handle json description element
496     *
497     * @param string $element <json_yxz key1=val1 ...>{ json_data }</json>
498     * @param string $xml_tag If $element is NULL, then $tag, $attributes and $content are parsed. Othervise ignored.
499     * @param string $xml_attributes same as $xml_tag
500     * @param string $xml_content same as $xml_tag, may be undefined also if $element is undefined.
501     *
502     * @return array [  tag => string,          //element tag name ('json_yxz' in above case)
503     *                  error => string,        //if set, then element is not valid
504     *                  keys => array,          //XML attributes parsed
505     *                  id => string,           //id of the element, '' if not set.
506     *                  json_inline_raw => string,  //internal JSON data as raw string, unverified.
507     *                  json_inline_extractors => array,  //extractors inside json_inline_raw string. Will be replaced by values from json database.
508     *                  path => ['clear' => bool, 'array' => bool, tokens => array] //from XML path attribute. It indicates, where and how data will be added to $json
509     *                  src => array['link', 'fragment', 'serverpath', 'internal_link'] //filename from XML src attribute
510     *                  src_path => array]      //from XML src_path attribute. It indicates, which part of src have to be added to $json.
511     */
512    public function handle_element($element, $xml_tag=NULL, $xml_attributes=NULL, $xml_content=NULL) {
513        //return value
514        $data = array();
515
516        if(isset($element)) {
517            //Replace #@macro_name@# patterns with strings defined by textinsert Plugin.
518            $element = helper_plugin_json::preprocess($element);
519
520            //parse element
521            if(preg_match('/^<(json[a-z0-9]*)\b([^>]*)>(.*)<\/\1>$/s', $element, $matches) !== 1) {
522                //this is more strict pattern than inside connectTo() function. Just ignore the element.
523                return NULL;
524            }
525            list( , $xml_tag, $xml_attributes, $xml_content) = array_pad($matches, 4, '');
526        }
527        else {
528            //Replace #@macro_name@# patterns with strings defined by textinsert Plugin.
529            if($xml_attributes !== '') {
530                $xml_attributes = helper_plugin_json::preprocess($xml_attributes);
531            }
532            if($xml_content !== '') {
533                $xml_content = helper_plugin_json::preprocess($xml_content);
534            }
535        }
536
537        $data['tag'] = $xml_tag;
538        if($xml_attributes !== '') {
539            $ret = helper_plugin_json::parse_key_val($xml_attributes);
540            if(is_array($ret)) {
541                $data['keys'] = $ret;
542                $data['id'] = $data['keys']['id'] ?? '';
543            }
544            else {
545                $err = "attributes syntax at $ret";
546            }
547        }
548        else if($xml_tag === '') {
549            $err = 'element syntax';
550        }
551
552        //get internal JSON data if present
553        $data['json_inline_raw'] = $xml_content;
554        $data['json_inline_extractors'] = helper_plugin_json::extractors_handle($xml_content);
555
556        //parse attribute 'path' and 'src_path' of the JSON object: -path.el2.0.el4[]
557        if(!isset($err)) {
558            if(isset($data['keys']['path'])) {
559                $val = $data['keys']['path'];
560
561                $val_name = array('clear' => false, 'array' => false);
562                if(substr($val, 0, 1) === '-') {
563                    $val = substr($val, 1);
564                    $val_name['clear'] = true;
565                }
566                if(substr($val, -2) === '[]') {
567                    $val = substr($val, 0, -2);
568                    $val_name['array'] = true;
569                }
570                $val_name['tokens'] = helper_plugin_json::parse_tokens($val);
571                $data['path'] = $val_name;
572            }
573            else {
574                $data['path'] = array('clear' => false, 'array' => false, 'tokens' => array());
575            }
576            if(isset($data['keys']['src_path'])) {
577                $data['src_path'] = helper_plugin_json::parse_tokens($data['keys']['src_path']);
578            }
579        }
580
581        //parse attribute 'src' with filepath to the JSON data
582        if(!isset($err) && isset($data['keys']['src'])) {
583            $val = $data['keys']['src'];
584
585            $data['src'] = helper_plugin_json::parse_src($val);
586            if(is_string($data['src'] ?? null)) {
587                $data['src_extractors'] = helper_plugin_json::extractors_handle($data['src']);
588            }
589            else if($data['src'] === false) {
590                $err = "'src' attribute syntax";
591            }
592        }
593
594        //If archive attribute contains json data, then this data will be
595        //loaded and src attribute will be ignored
596        if(!isset($err) && isset($data['keys']['archive'])) {
597            if(strtolower($data['keys']['archive']) === 'make') {
598                //data is is not yet archived. User action will write data from
599                //html 'json-data-original' into dokuwiki 'archive' attribute.
600                $data['make_archive'] = true;
601            }
602            else if(strtolower($data['keys']['archive']) === 'disable') {
603                //data is not yet archived. User action will disable
604                //'src', 'scr_ext' and 'archive' attributes
605                $data['make_archive'] = true;
606            }
607            else {
608                //get data from archive, not from src
609                $src_archive = json_decode($data['keys']['archive'], true);
610                if(isset($src_archive)) {
611                    $data['src_archive'] = $src_archive;
612                }
613                else {
614                    $err = '\'archive\' attribute syntax, internal JSON '.json_last_error_msg();
615                }
616            }
617        }
618
619        if(isset($err)) {
620            $data['error'] = $err;
621        }
622
623        return $data;
624    }
625
626
627    /**
628     * Add json data to database
629     *
630     * @param array $json_database External array, where data will be added (database).
631     * @param array $data Data from handle_element(). Two elements will be added:
632     *              'json_original' and 'json_combined'.
633     * @param integer $recursion_depth If greater than zero, then this function
634     *      will recursively decode <json>, if it has src attribute.
635     * @param array $log database with info about data sources for the element
636     *  [
637     *      'tag' => string,    //element tag name
638     *      'error' => string,  //error string or unset
639     *      'path' => string,   //data path
640     *      'inline' => bool,   //is inline data present
641     *      'src_archive' => bool,   //is src_archive data present
642     *      'src_path' => array,//path on src or unset
643     *      'src' => array [    //log about external files or unset
644     *          0 => [
645     *              'filename' => string,     //name of the internal or external link
646     *              'extenal_link' => bool,   //is 'filename' external link
647     *              'error' => string,        //error string or unset
648     *              'elements' => array [
649     *                  [                    //first element inside file
650     *                      recursive data (tag, error, path, etc.)
651     *                  ],
652     *                  [...], ...          //next elements
653     *              ]
654     *          ],
655     *          1 => [...], ...                 //next files
656     *      ]
657     *  ]
658     */
659    public function add_json(&$json_database, &$data, $recursion_depth, &$log) {
660        $path = $data['path'];
661        $src = isset($data['src']) ? $data['src'] : null;
662        $src_path = isset($data['src_path']) ? $data['src_path'] : null;
663
664        //set $json to specified path
665        $json = &$json_database;
666        foreach($path['tokens'] as $tok) {
667            $json_parent = &$json;
668            $json_parent_tok = $tok;
669            if(!is_array($json)) {
670                $json = array($tok => null);
671            }
672            $json = &$json[$tok];
673        }
674
675
676        //clear previous data, if necessary
677        if($path['clear']) {
678            $json = null;
679        }
680        else {
681            //don't remove the variable, if it is null on the end
682            unset($json_parent);
683            unset($json_parent_tok);
684        }
685
686
687        //load archived data
688        if(isset($data['src_archive'])) {
689            $json = $data['src_archive'];
690            $log['src_archive'] = true;
691        }
692        //or load JSON string from src attribute
693        else if(is_string($src)) {
694            //log, return value
695            $log['src'] = array();
696
697            $log['src_path'] = is_array($src_path) ? implode('.', $src_path) : '';
698            $log_file = array('filename' => '***JSON code***');
699
700            //replace extractors (%$path.to.var%) with data from json database
701            if(count($data['src_extractors']) > 0) {
702                $src = helper_plugin_json::extractors_replace(trim($src), $data['src_extractors'], $json_database);
703            }
704
705            $json_src = json_decode($src, true);
706            if(isset($json_src)) {
707                helper_plugin_json::add_data($json, $path['array'], $json_src, $src_path, $log_file);
708            }
709            else {
710                $log_file['error'] = 'JSON '.json_last_error_msg();
711            }
712            $log['src'][] = $log_file;
713        }
714        //or load data from external file(s), recursively if necessary
715        else if(is_array($src) && $recursion_depth > 0) {
716            //log, return value
717            $log['src'] = array();
718            $log['src_path'] = is_array($src_path) ? implode('.', $src_path) : '';
719
720            //prepare id for regular expression
721            $fragment_reg = (is_string($src['fragment'] ?? null) && $src['fragment'] !== '') ? 'id\s*=\s*'.$src['fragment'].'\b' : "";
722
723            //get file names
724            if(is_string($src['serverpath'] ?? null)) {
725                //internal link
726                $files = array();
727                foreach (glob($src['link'], GLOB_BRACE) as $filename) {
728                    if(is_file($filename)) {
729                        $files[] = $filename;
730                    }
731                }
732                if(count($files) === 0) {
733                    $filename_log = str_replace($src['serverpath'].'/', '', $src['link']);
734                    $filename_log = preg_replace('/\.txt$/', '', $filename_log);
735                    $filename_log = str_replace('/', ':', $filename_log);
736                    $log['src'][] = array('filename' => $filename_log, 'error' => 'file not found');
737                }
738            }
739            else {
740                //external link
741                $files = array($src['link']);
742            }
743
744            foreach ($files as $filename) {
745                $file_found = true;
746                $json_src = array();
747                $extenal_link = false;
748
749                //filename for write into log, file_id for authentication check
750                if(is_string($src['serverpath'] ?? null)) {
751                    //internal link
752                    $filename_log = str_replace($src['serverpath'].'/', '', $filename);
753                    $filename_log = preg_replace('/\.txt$/', '', $filename_log);
754                    $filename_log = str_replace('/', ':', $filename_log);
755                    $auth = (auth_quickaclcheck($filename_log) >= AUTH_READ);
756                    $log_file = array('filename' => $filename_log);
757                }
758                else {
759                    //external link (https://xxxx)
760                    $log_file = array('filename' => $filename, 'extenal_link' => true);
761                    $extenal_link = true;
762                    $auth = true;
763                }
764                if(is_string($src['fragment'] ?? null) && $src['fragment'] !== '') {
765                    $log_file['filename'] .= '#'.$src['fragment'];
766                }
767
768                //verify if user is authorized to read the file
769                if($auth === true) {
770                    if($extenal_link) {
771                        $curl = curl_init();
772                        curl_setopt($curl, CURLOPT_URL, $filename);
773                        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
774                        curl_setopt($curl, CURLOPT_HEADER, false);
775                        $text = curl_exec($curl);
776                        curl_close($curl);
777                    }
778                    else {
779                        $text = file_get_contents($filename);
780                    }
781
782                    if($text) {
783                        $json_src = json_decode($text, true);
784                        if(isset($json_src)) {
785                            //file with pure JSON data
786                            helper_plugin_json::add_data($json, $path['array'], $json_src, $src_path, $log_file);
787                        }
788                        else if(preg_match_all('/<(json[a-z0-9]*)\b([^>]*?'.$fragment_reg.'[^>]*?)>(.*?)<\/\1>/is',
789                                                $text, $matches_all, PREG_SET_ORDER)) {
790                            //find json data inside <json> element(s)
791                            $json_src = null;
792                            foreach ($matches_all as $matches) {
793                                $data_elem = helper_plugin_json::handle_element(NULL, $matches[1], $matches[2], $matches[3]);
794
795                                if($data_elem === NULL) {
796                                    continue;
797                                }
798
799                                $log_elem = array('tag' => $matches[1], 'id' => $data_elem['id'] ?? '', 'path' => $data_elem['keys']['path'] ?? null,
800                                                'inline' => (strlen(trim($data_elem['json_inline_raw'])) > 0));
801
802                                if(!isset($data_elem['error'])) {
803                                    //add json data into empty database
804                                    if(strlen($fragment_reg) > 0) {
805                                        //just pick one specific json data, don't buld database from whole file
806                                        $data_elem['path']['tokens'] = array();
807                                        $data_elem['path']['array'] = false;
808                                    }
809                                    helper_plugin_json::add_json($json_src, $data_elem, $recursion_depth-1, $log_elem);
810                                }
811                                else {
812                                    $log_elem['error'] = $data_elem['error'];
813                                }
814                                $log_file['elements'][] = $log_elem;
815                                if(strlen($fragment_reg) > 0) {
816                                    break;
817                                }
818                            }
819                            helper_plugin_json::add_data($json, $path['array'], $json_src, $src_path, $log_file);
820                        }
821                        else {
822                            $log_file['error'] = 'no JSON data, '.json_last_error_msg();
823                        }
824                    }
825                    else {
826                        $log_file['error'] = 'empty file';
827                    }
828                }
829                else {
830                    $log_file['error'] = 'file access denied';
831                }
832                $log['src'][] = $log_file;
833            }
834        }
835        else if(is_array($src)) {
836            //Error, recursion limit reached
837            if(is_string($src['serverpath'] ?? null)) {
838                //internal link
839                $filename_log = str_replace($src['serverpath'].'/', '', $src['link']);
840                $filename_log = preg_replace('/\.txt$/', '', $filename_log);
841                $filename_log = str_replace('/', ':', $filename_log);
842                $log['src'][] = array('filename' => $filename_log, 'error' => 'recursion limit reached');
843            }
844            else {
845                //external link (https://xxxx)
846                $log['src'][] = array('filename' => $src['link'], 'extenal_link' => true, 'error' => 'recursion limit reached');
847            }
848        }
849
850        //save original JSON data
851        $data['json_original'] = $json;
852
853
854        //load internal json data
855        $json_inline_raw = trim($data['json_inline_raw']);
856        if(strlen($json_inline_raw) > 0) {
857            //replace extractors (%$path.to.var%) with data from json database
858            if(count($data['json_inline_extractors']) > 0) {
859                $json_inline_raw = helper_plugin_json::extractors_replace($json_inline_raw, $data['json_inline_extractors'], $json_database);
860            }
861
862            $json_inline = json_decode($json_inline_raw, true);
863            if(isset($json_inline)) {
864                helper_plugin_json::add_data($json, $path['array'], $json_inline);
865            }
866            else {
867                $log['error'] = 'internal JSON '.json_last_error_msg();
868            }
869        }
870
871
872        //if element is empty, remove it from the database
873        if(!isset($json) && isset($json_parent)) {
874            unset($json_parent[$json_parent_tok]);
875        }
876
877        //save combined JSON data
878        $data['json_combined'] = $json;
879    }
880
881
882    /**
883     * Put data into database (array).
884     *
885     * @param array $path array of tokens for path in database.
886     * @param array $data to put in database.
887     * @param bool $clear first clear data from database path
888     * @param bool $append if true, append data to database path, othervise
889     *        combine data with array_replace_recursive() PHP function.
890     *
891     * @return data from the path
892     */
893    public function put($path, $data, $clear = false, $append = false) {
894        $json = &helper_plugin_json::$json;
895        foreach($path as $tok) {
896            $json_parent = &$json;
897            $json_parent_tok = $tok;
898
899            if(!(isset($json[$tok]) && is_array($json[$tok]))) {
900                $json[$tok] = array();
901            }
902            $json = &$json[$tok];
903        }
904
905        if($clear) {
906            $json = array();
907        }
908
909        if($append) {
910            $json[] = $data;
911        }
912        else if(is_array($data)) {
913            $json = array_replace_recursive($json, $data);
914        }
915
916        //if element is empty, remove it from array
917        if(count($json) === 0 && isset($json_parent)) {
918            unset($json_parent[$json_parent_tok]);
919        }
920
921        return $json;
922    }
923
924
925    /**
926     * Get data from database (array).
927     *
928     * @param array $path array of tokens for path in database.
929     * @param array $json_database If specified, it will be used for source data.
930     *              Othervise current JSON database will be used.
931     *
932     * @return mixed data
933     */
934    public function get($path = array(), $json_database = NULL) {
935        $var = NULL;
936        if(is_array($path)) {
937            $var = isset($json_database) ? $json_database : helper_plugin_json::$json;
938            foreach($path as $tok) {
939                if(!is_array($var)) {
940                    $var = NULL;
941                    break;
942                }
943                if ($tok === '_FIRST_') {
944                    $var = $var[array_key_first($var)];
945                }
946                else if ($tok === '_LAST_') {
947                    $var = $var[array_key_last($var)];
948                }
949                else {
950                    $var = $var[$tok] ?? null;
951                }
952            }
953        }
954        return $var;
955    }
956
957
958    /**
959     * Add data into database
960     *
961     * @param mixed $json_database - external array, where data will be added (database).
962     * @param bool  $append - if true, data will be appended, othervise it will be added or replaced.
963     * @param mixed $json_src - data to add.
964     * @param array $src_path - if defined, then only specific part of $json_src will be added.
965     * @param array $log - if error, it will be indicated here.
966     */
967    private function add_data(&$json_database, $append, $json_src, $src_path = null, &$log = null) {
968        if(is_array($src_path)) {
969            foreach($src_path as $tok) {
970                if(!isset($json_src[$tok])) {
971                    unset($json_src);
972                    break;
973                }
974                $json_src = $json_src[$tok];
975            }
976        }
977        if(isset($json_src)) {
978            if($append) {
979                if(is_array($json_database)) {
980                    $json_database[] = $json_src;
981                }
982                else {
983                    $json_database = array($json_src);
984                }
985            }
986            else {
987                if(is_array($json_database) && is_array($json_src)) {
988                    $json_database = array_replace_recursive($json_database, $json_src);
989                }
990                else {
991                    $json_database = $json_src;
992                }
993            }
994        }
995        else if(isset($log)) {
996            $log['error'] = 'no data';
997            if(is_array($src_path)) {
998                $log['error'] .= ' on src_path: \''.implode('.', $src_path).'\'';
999            }
1000        }
1001    }
1002
1003
1004    public function getMethods() {
1005        $result = array();
1006        $result[] = array(
1007            'name' => 'preprocess',
1008            'desc' => 'Preprocess matched string',
1009            'params' => array(
1010                'str' => 'string'
1011            ),
1012            'return' => 'string'
1013        );
1014        $result[] = array(
1015            'name' => 'parse_src',
1016            'desc' => 'Evaluate src attribute',
1017            'params' => array(
1018                'src' => 'string'
1019            ),
1020            'return' => array('link' => 'string', 'fragment' => 'string', 'serverpath' => 'string', 'internal_link' => 'string')
1021        );
1022        $result[] = array(
1023            'name' => 'parse_key_val',
1024            'desc' => 'parse a string into key - value pairs',
1025            'params' => array(
1026                'str' => 'string',
1027                'key_delim_val' => 'string',
1028                'key_val_delim' => 'string'
1029            ),
1030            'return' => array('pairs' => 'array', 'error' => 'int')
1031        );
1032        $result[] = array(
1033            'name' => 'parse_tokens',
1034            'desc' => 'parse tokens from string "tok1[tok2].tok3',
1035            'params' => array(
1036                'str' => 'string'
1037            ),
1038            'return' => array('tokens' => 'array')
1039        );
1040        $result[] = array(
1041            'name' => 'parse_links',
1042            'desc' => 'parse key=>tokenized_link pairs',
1043            'params' => array(
1044                'str' => 'string'
1045            ),
1046            'return' => array('pairs' => 'array', 'error' => 'bool', 'empty' => 'string')
1047        );
1048        $result[] = array(
1049            'name' => 'parse_filter',
1050            'desc' => 'parse filter expression for table',
1051            'params' => array(
1052                'str' => 'string'
1053            ),
1054            'return' => array('filter' => 'function', 'links' => 'array')
1055        );
1056        $result[] = array(
1057            'name' => 'filter',
1058            'desc' => 'verify filter for variable',
1059            'params' => array(
1060                'var' => 'array',
1061                'filter' => 'array'
1062            ),
1063            'return' => 'boolean'
1064        );
1065        $result[] = array(
1066            'name' => 'extractors_handle',
1067            'desc' => 'handle extractors inside string',
1068            'params' => array(
1069                'str' => 'string'
1070            ),
1071            'return' => array('tokens' => 'array', 'row_filter' => 'array', 'row_inserts' => 'array', 'filter' => 'array')
1072        );
1073        $result[] = array(
1074            'name' => 'extractors_replace',
1075            'desc' => 'replace extractors inside string',
1076            'params' => array(
1077                'str' => 'array',
1078                'filter' => 'array'
1079            ),
1080            'return' => 'string'
1081        );
1082        $result[] = array(
1083            'name' => 'add_json',
1084            'desc' => 'Add json data to database',
1085            'params' => array(
1086                'json_database' => 'array',
1087                'data' => 'array',
1088                'recursion_depth' => 'integer',
1089                'log' => 'array'
1090            )
1091        );
1092        $result[] = array(
1093            'name' => 'put',
1094            'desc' => 'put data into database',
1095            'params' => array(
1096                'path' => 'array',
1097                'data' => 'array',
1098                'clear' => 'bool',
1099                'append' => 'bool'
1100            ),
1101            'return' => array('data' => 'array|NULL')
1102        );
1103        $result[] = array(
1104            'name' => 'get',
1105            'desc' => 'get data from database',
1106            'params' => array(
1107                'path' => 'array',
1108                'json_database' => 'array'
1109            ),
1110            'return' => array('data' => 'mixed|NULL')
1111        );
1112        return $result;
1113    }
1114}
1115