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