1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Report; 21 22use DomainException; 23use Fisharebest\Webtrees\Auth; 24use Fisharebest\Webtrees\Carbon; 25use Fisharebest\Webtrees\Date; 26use Fisharebest\Webtrees\Elements\UnknownElement; 27use Fisharebest\Webtrees\Family; 28use Fisharebest\Webtrees\Gedcom; 29use Fisharebest\Webtrees\GedcomRecord; 30use Fisharebest\Webtrees\I18N; 31use Fisharebest\Webtrees\Individual; 32use Fisharebest\Webtrees\Log; 33use Fisharebest\Webtrees\MediaFile; 34use Fisharebest\Webtrees\Note; 35use Fisharebest\Webtrees\Place; 36use Fisharebest\Webtrees\Registry; 37use Fisharebest\Webtrees\Tree; 38use Illuminate\Database\Capsule\Manager as DB; 39use Illuminate\Database\Query\Builder; 40use Illuminate\Database\Query\Expression; 41use Illuminate\Database\Query\JoinClause; 42use Illuminate\Support\Str; 43use League\Flysystem\FilesystemOperator; 44use LogicException; 45use Symfony\Component\Cache\Adapter\NullAdapter; 46use Symfony\Component\ExpressionLanguage\ExpressionLanguage; 47use XMLParser; 48 49use function addcslashes; 50use function addslashes; 51use function array_pop; 52use function array_shift; 53use function assert; 54use function call_user_func; 55use function count; 56use function end; 57use function explode; 58use function file; 59use function file_exists; 60use function getimagesize; 61use function imagecreatefromstring; 62use function imagesx; 63use function imagesy; 64use function in_array; 65use function method_exists; 66use function preg_match; 67use function preg_match_all; 68use function preg_replace; 69use function preg_replace_callback; 70use function preg_split; 71use function reset; 72use function round; 73use function sprintf; 74use function str_contains; 75use function str_replace; 76use function str_starts_with; 77use function strip_tags; 78use function strlen; 79use function strtoupper; 80use function substr; 81use function trim; 82use function uasort; 83use function xml_error_string; 84use function xml_get_current_line_number; 85use function xml_get_error_code; 86use function xml_parse; 87use function xml_parser_create; 88use function xml_parser_free; 89use function xml_parser_set_option; 90use function xml_set_character_data_handler; 91use function xml_set_element_handler; 92 93use const PREG_SET_ORDER; 94use const XML_OPTION_CASE_FOLDING; 95 96/** 97 * Class ReportParserGenerate - parse a report.xml file and generate the report. 98 */ 99class ReportParserGenerate extends ReportParserBase 100{ 101 /** @var bool Are we collecting data from <Footnote> elements */ 102 private $process_footnote = true; 103 104 /** @var bool Are we currently outputing data? */ 105 private $print_data = false; 106 107 /** @var bool[] Push-down stack of $print_data */ 108 private $print_data_stack = []; 109 110 /** @var int Are we processing GEDCOM data */ 111 private $process_gedcoms = 0; 112 113 /** @var int Are we processing conditionals */ 114 private $process_ifs = 0; 115 116 /** @var int Are we processing repeats */ 117 private $process_repeats = 0; 118 119 /** @var int Quantity of data to repeat during loops */ 120 private $repeat_bytes = 0; 121 122 /** @var array<string> Repeated data when iterating over loops */ 123 private array $repeats = []; 124 125 /** @var array<int,array<int,array<string>|int>> Nested repeating data */ 126 private array $repeats_stack = []; 127 128 /** @var array<AbstractRenderer> Nested repeating data */ 129 private array $wt_report_stack = []; 130 131 /** @var XMLParser (resource before PHP 8.0) Nested repeating data */ 132 private $parser; 133 134 /** @var XMLParser[] (resource[] before PHP 8.0) Nested repeating data */ 135 private array $parser_stack = []; 136 137 /** @var string The current GEDCOM record */ 138 private $gedrec = ''; 139 140 /** @var array<string> Nested GEDCOM records */ 141 private array $gedrec_stack = []; 142 143 /** @var ReportBaseElement The currently processed element */ 144 private $current_element; 145 146 /** @var ReportBaseElement The currently processed element */ 147 private $footnote_element; 148 149 /** @var string The GEDCOM fact currently being processed */ 150 private $fact = ''; 151 152 /** @var string The GEDCOM value currently being processed */ 153 private $desc = ''; 154 155 /** @var string The GEDCOM type currently being processed */ 156 private $type = ''; 157 158 /** @var int The current generational level */ 159 private $generation = 1; 160 161 /** @var array Source data for processing lists */ 162 private array $list = []; 163 164 /** @var int Number of items in lists */ 165 private $list_total = 0; 166 167 /** @var int Number of items filtered from lists */ 168 private $list_private = 0; 169 170 /** @var string The filename of the XML report */ 171 protected $report; 172 173 /** @var AbstractRenderer A factory for creating report elements */ 174 private $report_root; 175 176 /** @var AbstractRenderer Nested report elements */ 177 private $wt_report; 178 179 /** @var array<array<string>> Variables defined in the report at run-time */ 180 private array $vars; 181 182 private Tree $tree; 183 184 private FilesystemOperator $data_filesystem; 185 186 /** 187 * Create a parser for a report 188 * 189 * @param string $report The XML filename 190 * @param AbstractRenderer $report_root 191 * @param array<array<string>> $vars 192 * @param Tree $tree 193 * @param FilesystemOperator $data_filesystem 194 */ 195 public function __construct( 196 string $report, 197 AbstractRenderer $report_root, 198 array $vars, 199 Tree $tree, 200 FilesystemOperator $data_filesystem 201 ) { 202 $this->report = $report; 203 $this->report_root = $report_root; 204 $this->wt_report = $report_root; 205 $this->current_element = new ReportBaseElement(); 206 $this->vars = $vars; 207 $this->tree = $tree; 208 $this->data_filesystem = $data_filesystem; 209 210 parent::__construct($report); 211 } 212 213 /** 214 * get a gedcom subrecord 215 * 216 * searches a gedcom record and returns a subrecord of it. A subrecord is defined starting at a 217 * line with level N and all subsequent lines greater than N until the next N level is reached. 218 * For example, the following is a BIRT subrecord: 219 * <code>1 BIRT 220 * 2 DATE 1 JAN 1900 221 * 2 PLAC Phoenix, Maricopa, Arizona</code> 222 * The following example is the DATE subrecord of the above BIRT subrecord: 223 * <code>2 DATE 1 JAN 1900</code> 224 * 225 * @param int $level the N level of the subrecord to get 226 * @param string $tag a gedcom tag or string to search for in the record (ie 1 BIRT or 2 DATE) 227 * @param string $gedrec the parent gedcom record to search in 228 * @param int $num this allows you to specify which matching <var>$tag</var> to get. Oftentimes a 229 * gedcom record will have more that 1 of the same type of subrecord. An individual may have 230 * multiple events for example. Passing $num=1 would get the first 1. Passing $num=2 would get the 231 * second one, etc. 232 * 233 * @return string the subrecord that was found or an empty string "" if not found. 234 */ 235 public static function getSubRecord(int $level, string $tag, string $gedrec, int $num = 1): string 236 { 237 if ($gedrec === '') { 238 return ''; 239 } 240 // -- adding \n before and after gedrec 241 $gedrec = "\n" . $gedrec . "\n"; 242 $tag = trim($tag); 243 $searchTarget = "~[\n]" . $tag . "[\s]~"; 244 $ct = preg_match_all($searchTarget, $gedrec, $match, PREG_SET_ORDER | PREG_OFFSET_CAPTURE); 245 if ($ct === 0) { 246 return ''; 247 } 248 if ($ct < $num) { 249 return ''; 250 } 251 $pos1 = $match[$num - 1][0][1]; 252 $pos2 = strpos($gedrec, "\n$level", $pos1 + 1); 253 if (!$pos2) { 254 $pos2 = strpos($gedrec, "\n1", $pos1 + 1); 255 } 256 if (!$pos2) { 257 $pos2 = strpos($gedrec, "\nWT_", $pos1 + 1); // WT_SPOUSE, WT_FAMILY_ID ... 258 } 259 if (!$pos2) { 260 return ltrim(substr($gedrec, $pos1)); 261 } 262 $subrec = substr($gedrec, $pos1, $pos2 - $pos1); 263 264 return ltrim($subrec); 265 } 266 267 /** 268 * get CONT lines 269 * 270 * get the N+1 CONT or CONC lines of a gedcom subrecord 271 * 272 * @param int $nlevel the level of the CONT lines to get 273 * @param string $nrec the gedcom subrecord to search in 274 * 275 * @return string a string with all CONT lines merged 276 */ 277 public static function getCont(int $nlevel, string $nrec): string 278 { 279 $text = ''; 280 281 $subrecords = explode("\n", $nrec); 282 foreach ($subrecords as $thisSubrecord) { 283 if (substr($thisSubrecord, 0, 2) !== $nlevel . ' ') { 284 continue; 285 } 286 $subrecordType = substr($thisSubrecord, 2, 4); 287 if ($subrecordType === 'CONT') { 288 $text .= "\n" . substr($thisSubrecord, 7); 289 } 290 } 291 292 return $text; 293 } 294 295 /** 296 * XML start element handler 297 * This function is called whenever a starting element is reached 298 * The element handler will be called if found, otherwise it must be HTML 299 * 300 * @param resource $parser the resource handler for the XML parser 301 * @param string $name the name of the XML element parsed 302 * @param array<string> $attrs an array of key value pairs for the attributes 303 * 304 * @return void 305 */ 306 protected function startElement($parser, string $name, array $attrs): void 307 { 308 $newattrs = []; 309 310 foreach ($attrs as $key => $value) { 311 if (preg_match("/^\\$(\w+)$/", $value, $match)) { 312 if (isset($this->vars[$match[1]]['id']) && !isset($this->vars[$match[1]]['gedcom'])) { 313 $value = $this->vars[$match[1]]['id']; 314 } 315 } 316 $newattrs[$key] = $value; 317 } 318 $attrs = $newattrs; 319 if ($this->process_footnote && ($this->process_ifs === 0 || $name === 'if') && ($this->process_gedcoms === 0 || $name === 'Gedcom') && ($this->process_repeats === 0 || $name === 'Facts' || $name === 'RepeatTag')) { 320 $method = $name . 'StartHandler'; 321 322 if (method_exists($this, $method)) { 323 call_user_func([$this, $method], $attrs); 324 } 325 } 326 } 327 328 /** 329 * XML end element handler 330 * This function is called whenever an ending element is reached 331 * The element handler will be called if found, otherwise it must be HTML 332 * 333 * @param resource $parser the resource handler for the XML parser 334 * @param string $name the name of the XML element parsed 335 * 336 * @return void 337 */ 338 protected function endElement($parser, string $name): void 339 { 340 if (($this->process_footnote || $name === 'Footnote') && ($this->process_ifs === 0 || $name === 'if') && ($this->process_gedcoms === 0 || $name === 'Gedcom') && ($this->process_repeats === 0 || $name === 'Facts' || $name === 'RepeatTag' || $name === 'List' || $name === 'Relatives')) { 341 $method = $name . 'EndHandler'; 342 343 if (method_exists($this, $method)) { 344 call_user_func([$this, $method]); 345 } 346 } 347 } 348 349 /** 350 * XML character data handler 351 * 352 * @param resource $parser the resource handler for the XML parser 353 * @param string $data the name of the XML element parsed 354 * 355 * @return void 356 */ 357 protected function characterData($parser, string $data): void 358 { 359 if ($this->print_data && $this->process_gedcoms === 0 && $this->process_ifs === 0 && $this->process_repeats === 0) { 360 $this->current_element->addText($data); 361 } 362 } 363 364 /** 365 * Handle <style> 366 * 367 * @param array<string> $attrs 368 * 369 * @return void 370 */ 371 protected function styleStartHandler(array $attrs): void 372 { 373 if (empty($attrs['name'])) { 374 throw new DomainException('REPORT ERROR Style: The "name" of the style is missing or not set in the XML file.'); 375 } 376 377 // array Style that will be passed on 378 $s = []; 379 380 // string Name af the style 381 $s['name'] = $attrs['name']; 382 383 // string Name of the DEFAULT font 384 $s['font'] = $this->wt_report->default_font; 385 if (!empty($attrs['font'])) { 386 $s['font'] = $attrs['font']; 387 } 388 389 // int The size of the font in points 390 $s['size'] = $this->wt_report->default_font_size; 391 if (!empty($attrs['size'])) { 392 // Get it as int to ignore all decimal points or text (if no text then 0) 393 $s['size'] = (string) (int) $attrs['size']; 394 } 395 396 // string B: bold, I: italic, U: underline, D: line trough, The default value is regular. 397 $s['style'] = ''; 398 if (!empty($attrs['style'])) { 399 $s['style'] = $attrs['style']; 400 } 401 402 $this->wt_report->addStyle($s); 403 } 404 405 /** 406 * Handle <doc> 407 * Sets up the basics of the document proparties 408 * 409 * @param array<string> $attrs 410 * 411 * @return void 412 */ 413 protected function docStartHandler(array $attrs): void 414 { 415 $this->parser = $this->xml_parser; 416 417 // Custom page width 418 if (!empty($attrs['customwidth'])) { 419 $this->wt_report->page_width = (int) $attrs['customwidth']; 420 } // Get it as int to ignore all decimal points or text (if any text then int(0)) 421 // Custom Page height 422 if (!empty($attrs['customheight'])) { 423 $this->wt_report->page_height = (int) $attrs['customheight']; 424 } // Get it as int to ignore all decimal points or text (if any text then int(0)) 425 426 // Left Margin 427 if (isset($attrs['leftmargin'])) { 428 if ($attrs['leftmargin'] === '0') { 429 $this->wt_report->left_margin = 0; 430 } elseif (!empty($attrs['leftmargin'])) { 431 $this->wt_report->left_margin = (int) $attrs['leftmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0)) 432 } 433 } 434 // Right Margin 435 if (isset($attrs['rightmargin'])) { 436 if ($attrs['rightmargin'] === '0') { 437 $this->wt_report->right_margin = 0; 438 } elseif (!empty($attrs['rightmargin'])) { 439 $this->wt_report->right_margin = (int) $attrs['rightmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0)) 440 } 441 } 442 // Top Margin 443 if (isset($attrs['topmargin'])) { 444 if ($attrs['topmargin'] === '0') { 445 $this->wt_report->top_margin = 0; 446 } elseif (!empty($attrs['topmargin'])) { 447 $this->wt_report->top_margin = (int) $attrs['topmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0)) 448 } 449 } 450 // Bottom Margin 451 if (isset($attrs['bottommargin'])) { 452 if ($attrs['bottommargin'] === '0') { 453 $this->wt_report->bottom_margin = 0; 454 } elseif (!empty($attrs['bottommargin'])) { 455 $this->wt_report->bottom_margin = (int) $attrs['bottommargin']; // Get it as int to ignore all decimal points or text (if any text then int(0)) 456 } 457 } 458 // Header Margin 459 if (isset($attrs['headermargin'])) { 460 if ($attrs['headermargin'] === '0') { 461 $this->wt_report->header_margin = 0; 462 } elseif (!empty($attrs['headermargin'])) { 463 $this->wt_report->header_margin = (int) $attrs['headermargin']; // Get it as int to ignore all decimal points or text (if any text then int(0)) 464 } 465 } 466 // Footer Margin 467 if (isset($attrs['footermargin'])) { 468 if ($attrs['footermargin'] === '0') { 469 $this->wt_report->footer_margin = 0; 470 } elseif (!empty($attrs['footermargin'])) { 471 $this->wt_report->footer_margin = (int) $attrs['footermargin']; // Get it as int to ignore all decimal points or text (if any text then int(0)) 472 } 473 } 474 475 // Page Orientation 476 if (!empty($attrs['orientation'])) { 477 if ($attrs['orientation'] === 'landscape') { 478 $this->wt_report->orientation = 'landscape'; 479 } elseif ($attrs['orientation'] === 'portrait') { 480 $this->wt_report->orientation = 'portrait'; 481 } 482 } 483 // Page Size 484 if (!empty($attrs['pageSize'])) { 485 $this->wt_report->page_format = strtoupper($attrs['pageSize']); 486 } 487 488 // Show Generated By... 489 if (isset($attrs['showGeneratedBy'])) { 490 if ($attrs['showGeneratedBy'] === '0') { 491 $this->wt_report->show_generated_by = false; 492 } elseif ($attrs['showGeneratedBy'] === '1') { 493 $this->wt_report->show_generated_by = true; 494 } 495 } 496 497 $this->wt_report->setup(); 498 } 499 500 /** 501 * Handle </doc> 502 * 503 * @return void 504 */ 505 protected function docEndHandler(): void 506 { 507 $this->wt_report->run(); 508 } 509 510 /** 511 * Handle <header> 512 * 513 * @return void 514 */ 515 protected function headerStartHandler(): void 516 { 517 // Clear the Header before any new elements are added 518 $this->wt_report->clearHeader(); 519 $this->wt_report->setProcessing('H'); 520 } 521 522 /** 523 * Handle <body> 524 * 525 * @return void 526 */ 527 protected function bodyStartHandler(): void 528 { 529 $this->wt_report->setProcessing('B'); 530 } 531 532 /** 533 * Handle <footer> 534 * 535 * @return void 536 */ 537 protected function footerStartHandler(): void 538 { 539 $this->wt_report->setProcessing('F'); 540 } 541 542 /** 543 * Handle <cell> 544 * 545 * @param array<string,string> $attrs 546 * 547 * @return void 548 */ 549 protected function cellStartHandler(array $attrs): void 550 { 551 // string The text alignment of the text in this box. 552 $align = $attrs['align'] ?? ''; 553 // RTL supported left/right alignment 554 if ($align === 'rightrtl') { 555 if ($this->wt_report->rtl) { 556 $align = 'left'; 557 } else { 558 $align = 'right'; 559 } 560 } elseif ($align === 'leftrtl') { 561 if ($this->wt_report->rtl) { 562 $align = 'right'; 563 } else { 564 $align = 'left'; 565 } 566 } 567 568 // The color to fill the background of this cell 569 $bgcolor = $attrs['bgcolor'] ?? ''; 570 571 // Whether the background should be painted 572 $fill = (int) ($attrs['fill'] ?? '0'); 573 574 // If true reset the last cell height 575 $reseth = (bool) ($attrs['reseth'] ?? '1'); 576 577 // Whether a border should be printed around this box 578 $border = $attrs['border'] ?? ''; 579 580 // string Border color in HTML code 581 $bocolor = $attrs['bocolor'] ?? ''; 582 583 // Cell height (expressed in points) The starting height of this cell. If the text wraps the height will automatically be adjusted. 584 $height = (int) ($attrs['height'] ?? '0'); 585 586 // int Cell width (expressed in points) Setting the width to 0 will make it the width from the current location to the right margin. 587 $width = (int) ($attrs['width'] ?? '0'); 588 589 // Stretch character mode 590 $stretch = (int) ($attrs['stretch'] ?? '0'); 591 592 // mixed Position the left corner of this box on the page. The default is the current position. 593 $left = ReportBaseElement::CURRENT_POSITION; 594 if (isset($attrs['left'])) { 595 if ($attrs['left'] === '.') { 596 $left = ReportBaseElement::CURRENT_POSITION; 597 } elseif (!empty($attrs['left'])) { 598 $left = (int) $attrs['left']; 599 } elseif ($attrs['left'] === '0') { 600 $left = 0; 601 } 602 } 603 // mixed Position the top corner of this box on the page. the default is the current position 604 $top = ReportBaseElement::CURRENT_POSITION; 605 if (isset($attrs['top'])) { 606 if ($attrs['top'] === '.') { 607 $top = ReportBaseElement::CURRENT_POSITION; 608 } elseif (!empty($attrs['top'])) { 609 $top = (int) $attrs['top']; 610 } elseif ($attrs['top'] === '0') { 611 $top = 0; 612 } 613 } 614 615 // The name of the Style that should be used to render the text. 616 $style = $attrs['style'] ?? ''; 617 618 // string Text color in html code 619 $tcolor = $attrs['tcolor'] ?? ''; 620 621 // int Indicates where the current position should go after the call. 622 $ln = 0; 623 if (isset($attrs['newline'])) { 624 if (!empty($attrs['newline'])) { 625 $ln = (int) $attrs['newline']; 626 } elseif ($attrs['newline'] === '0') { 627 $ln = 0; 628 } 629 } 630 631 if ($align === 'left') { 632 $align = 'L'; 633 } elseif ($align === 'right') { 634 $align = 'R'; 635 } elseif ($align === 'center') { 636 $align = 'C'; 637 } elseif ($align === 'justify') { 638 $align = 'J'; 639 } 640 641 $this->print_data_stack[] = $this->print_data; 642 $this->print_data = true; 643 644 $this->current_element = $this->report_root->createCell( 645 (int) $width, 646 (int) $height, 647 $border, 648 $align, 649 $bgcolor, 650 $style, 651 $ln, 652 $top, 653 $left, 654 $fill, 655 $stretch, 656 $bocolor, 657 $tcolor, 658 $reseth 659 ); 660 } 661 662 /** 663 * Handle </cell> 664 * 665 * @return void 666 */ 667 protected function cellEndHandler(): void 668 { 669 $this->print_data = array_pop($this->print_data_stack); 670 $this->wt_report->addElement($this->current_element); 671 } 672 673 /** 674 * Handle <now /> 675 * 676 * @return void 677 */ 678 protected function nowStartHandler(): void 679 { 680 $this->current_element->addText(Carbon::now()->local()->isoFormat('LLLL')); 681 } 682 683 /** 684 * Handle <pageNum /> 685 * 686 * @return void 687 */ 688 protected function pageNumStartHandler(): void 689 { 690 $this->current_element->addText('#PAGENUM#'); 691 } 692 693 /** 694 * Handle <totalPages /> 695 * 696 * @return void 697 */ 698 protected function totalPagesStartHandler(): void 699 { 700 $this->current_element->addText('{{:ptp:}}'); 701 } 702 703 /** 704 * Called at the start of an element. 705 * 706 * @param array<string> $attrs an array of key value pairs for the attributes 707 * 708 * @return void 709 */ 710 protected function gedcomStartHandler(array $attrs): void 711 { 712 if ($this->process_gedcoms > 0) { 713 $this->process_gedcoms++; 714 715 return; 716 } 717 718 $tag = $attrs['id']; 719 $tag = str_replace('@fact', $this->fact, $tag); 720 $tags = explode(':', $tag); 721 $newgedrec = ''; 722 if (count($tags) < 2) { 723 $tmp = Registry::gedcomRecordFactory()->make($attrs['id'], $this->tree); 724 $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($this->tree)) : ''; 725 } 726 if (empty($newgedrec)) { 727 $tgedrec = $this->gedrec; 728 $newgedrec = ''; 729 foreach ($tags as $tag) { 730 if (preg_match('/\$(.+)/', $tag, $match)) { 731 if (isset($this->vars[$match[1]]['gedcom'])) { 732 $newgedrec = $this->vars[$match[1]]['gedcom']; 733 } else { 734 $tmp = Registry::gedcomRecordFactory()->make($match[1], $this->tree); 735 $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($this->tree)) : ''; 736 } 737 } else { 738 if (preg_match('/@(.+)/', $tag, $match)) { 739 $gmatch = []; 740 if (preg_match("/\d $match[1] @([^@]+)@/", $tgedrec, $gmatch)) { 741 $tmp = Registry::gedcomRecordFactory()->make($gmatch[1], $this->tree); 742 $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($this->tree)) : ''; 743 $tgedrec = $newgedrec; 744 } else { 745 $newgedrec = ''; 746 break; 747 } 748 } else { 749 $level = 1 + (int) explode(' ', trim($tgedrec))[0]; 750 $newgedrec = self::getSubRecord($level, "$level $tag", $tgedrec); 751 $tgedrec = $newgedrec; 752 } 753 } 754 } 755 } 756 if (!empty($newgedrec)) { 757 $this->gedrec_stack[] = [$this->gedrec, $this->fact, $this->desc]; 758 $this->gedrec = $newgedrec; 759 if (preg_match("/(\d+) (_?[A-Z0-9]+) (.*)/", $this->gedrec, $match)) { 760 $this->fact = $match[2]; 761 $this->desc = trim($match[3]); 762 } 763 } else { 764 $this->process_gedcoms++; 765 } 766 } 767 768 /** 769 * Called at the end of an element. 770 * 771 * @return void 772 */ 773 protected function gedcomEndHandler(): void 774 { 775 if ($this->process_gedcoms > 0) { 776 $this->process_gedcoms--; 777 } else { 778 [$this->gedrec, $this->fact, $this->desc] = array_pop($this->gedrec_stack); 779 } 780 } 781 782 /** 783 * Handle <textBox> 784 * 785 * @param array<string> $attrs 786 * 787 * @return void 788 */ 789 protected function textBoxStartHandler(array $attrs): void 790 { 791 // string Background color code 792 $bgcolor = ''; 793 if (!empty($attrs['bgcolor'])) { 794 $bgcolor = $attrs['bgcolor']; 795 } 796 797 // boolean Wether or not fill the background color 798 $fill = true; 799 if (isset($attrs['fill'])) { 800 if ($attrs['fill'] === '0') { 801 $fill = false; 802 } elseif ($attrs['fill'] === '1') { 803 $fill = true; 804 } 805 } 806 807 // var boolean Whether or not a border should be printed around this box. 0 = no border, 1 = border. Default is 0 808 $border = false; 809 if (isset($attrs['border'])) { 810 if ($attrs['border'] === '1') { 811 $border = true; 812 } elseif ($attrs['border'] === '0') { 813 $border = false; 814 } 815 } 816 817 // int The starting height of this cell. If the text wraps the height will automatically be adjusted 818 $height = 0; 819 if (!empty($attrs['height'])) { 820 $height = (int) $attrs['height']; 821 } 822 // int Setting the width to 0 will make it the width from the current location to the margin 823 $width = 0; 824 if (!empty($attrs['width'])) { 825 $width = (int) $attrs['width']; 826 } 827 828 // mixed Position the left corner of this box on the page. The default is the current position. 829 $left = ReportBaseElement::CURRENT_POSITION; 830 if (isset($attrs['left'])) { 831 if ($attrs['left'] === '.') { 832 $left = ReportBaseElement::CURRENT_POSITION; 833 } elseif (!empty($attrs['left'])) { 834 $left = (int) $attrs['left']; 835 } elseif ($attrs['left'] === '0') { 836 $left = 0; 837 } 838 } 839 // mixed Position the top corner of this box on the page. the default is the current position 840 $top = ReportBaseElement::CURRENT_POSITION; 841 if (isset($attrs['top'])) { 842 if ($attrs['top'] === '.') { 843 $top = ReportBaseElement::CURRENT_POSITION; 844 } elseif (!empty($attrs['top'])) { 845 $top = (int) $attrs['top']; 846 } elseif ($attrs['top'] === '0') { 847 $top = 0; 848 } 849 } 850 // boolean After this box is finished rendering, should the next section of text start immediately after the this box or should it start on a new line under this box. 0 = no new line, 1 = force new line. Default is 0 851 $newline = false; 852 if (isset($attrs['newline'])) { 853 if ($attrs['newline'] === '1') { 854 $newline = true; 855 } elseif ($attrs['newline'] === '0') { 856 $newline = false; 857 } 858 } 859 // boolean 860 $pagecheck = true; 861 if (isset($attrs['pagecheck'])) { 862 if ($attrs['pagecheck'] === '0') { 863 $pagecheck = false; 864 } elseif ($attrs['pagecheck'] === '1') { 865 $pagecheck = true; 866 } 867 } 868 // boolean Cell padding 869 $padding = true; 870 if (isset($attrs['padding'])) { 871 if ($attrs['padding'] === '0') { 872 $padding = false; 873 } elseif ($attrs['padding'] === '1') { 874 $padding = true; 875 } 876 } 877 // boolean Reset this box Height 878 $reseth = false; 879 if (isset($attrs['reseth'])) { 880 if ($attrs['reseth'] === '1') { 881 $reseth = true; 882 } elseif ($attrs['reseth'] === '0') { 883 $reseth = false; 884 } 885 } 886 887 // string Style of rendering 888 $style = ''; 889 890 $this->print_data_stack[] = $this->print_data; 891 $this->print_data = false; 892 893 $this->wt_report_stack[] = $this->wt_report; 894 $this->wt_report = $this->report_root->createTextBox( 895 $width, 896 $height, 897 $border, 898 $bgcolor, 899 $newline, 900 $left, 901 $top, 902 $pagecheck, 903 $style, 904 $fill, 905 $padding, 906 $reseth 907 ); 908 } 909 910 /** 911 * Handle <textBox> 912 * 913 * @return void 914 */ 915 protected function textBoxEndHandler(): void 916 { 917 $this->print_data = array_pop($this->print_data_stack); 918 $this->current_element = $this->wt_report; 919 920 // The TextBox handler is mis-using the wt_report attribute to store an element. 921 // Until this can be re-designed, we need this assertion to help static analysis tools. 922 assert($this->current_element instanceof ReportBaseElement, new LogicException()); 923 924 $this->wt_report = array_pop($this->wt_report_stack); 925 $this->wt_report->addElement($this->current_element); 926 } 927 928 /** 929 * XLM <Text>. 930 * 931 * @param array<string> $attrs an array of key value pairs for the attributes 932 * 933 * @return void 934 */ 935 protected function textStartHandler(array $attrs): void 936 { 937 $this->print_data_stack[] = $this->print_data; 938 $this->print_data = true; 939 940 // string The name of the Style that should be used to render the text. 941 $style = ''; 942 if (!empty($attrs['style'])) { 943 $style = $attrs['style']; 944 } 945 946 // string The color of the text - Keep the black color as default 947 $color = ''; 948 if (!empty($attrs['color'])) { 949 $color = $attrs['color']; 950 } 951 952 $this->current_element = $this->report_root->createText($style, $color); 953 } 954 955 /** 956 * Handle </text> 957 * 958 * @return void 959 */ 960 protected function textEndHandler(): void 961 { 962 $this->print_data = array_pop($this->print_data_stack); 963 $this->wt_report->addElement($this->current_element); 964 } 965 966 /** 967 * Handle <getPersonName /> 968 * Get the name 969 * 1. id is empty - current GEDCOM record 970 * 2. id is set with a record id 971 * 972 * @param array<string> $attrs an array of key value pairs for the attributes 973 * 974 * @return void 975 */ 976 protected function getPersonNameStartHandler(array $attrs): void 977 { 978 $id = ''; 979 $match = []; 980 if (empty($attrs['id'])) { 981 if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) { 982 $id = $match[1]; 983 } 984 } else { 985 if (preg_match('/\$(.+)/', $attrs['id'], $match)) { 986 if (isset($this->vars[$match[1]]['id'])) { 987 $id = $this->vars[$match[1]]['id']; 988 } 989 } else { 990 if (preg_match('/@(.+)/', $attrs['id'], $match)) { 991 $gmatch = []; 992 if (preg_match("/\d $match[1] @([^@]+)@/", $this->gedrec, $gmatch)) { 993 $id = $gmatch[1]; 994 } 995 } else { 996 $id = $attrs['id']; 997 } 998 } 999 } 1000 if (!empty($id)) { 1001 $record = Registry::gedcomRecordFactory()->make($id, $this->tree); 1002 if ($record === null) { 1003 return; 1004 } 1005 if (!$record->canShowName()) { 1006 $this->current_element->addText(I18N::translate('Private')); 1007 } else { 1008 $name = $record->fullName(); 1009 $name = strip_tags($name); 1010 if (!empty($attrs['truncate'])) { 1011 $name = Str::limit($name, (int) $attrs['truncate'], I18N::translate('…')); 1012 } else { 1013 $addname = (string) $record->alternateName(); 1014 $addname = strip_tags($addname); 1015 if (!empty($addname)) { 1016 $name .= ' ' . $addname; 1017 } 1018 } 1019 $this->current_element->addText(trim($name)); 1020 } 1021 } 1022 } 1023 1024 /** 1025 * Handle <gedcomValue /> 1026 * 1027 * @param array<string> $attrs 1028 * 1029 * @return void 1030 */ 1031 protected function gedcomValueStartHandler(array $attrs): void 1032 { 1033 $id = ''; 1034 $match = []; 1035 if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) { 1036 $id = $match[1]; 1037 } 1038 1039 if (isset($attrs['newline']) && $attrs['newline'] === '1') { 1040 $useBreak = '1'; 1041 } else { 1042 $useBreak = '0'; 1043 } 1044 1045 $tag = $attrs['tag']; 1046 if (!empty($tag)) { 1047 if ($tag === '@desc') { 1048 $value = $this->desc; 1049 $value = trim($value); 1050 $this->current_element->addText($value); 1051 } 1052 if ($tag === '@id') { 1053 $this->current_element->addText($id); 1054 } else { 1055 $tag = str_replace('@fact', $this->fact, $tag); 1056 if (empty($attrs['level'])) { 1057 $level = (int) explode(' ', trim($this->gedrec))[0]; 1058 if ($level === 0) { 1059 $level++; 1060 } 1061 } else { 1062 $level = (int) $attrs['level']; 1063 } 1064 $tags = preg_split('/[: ]/', $tag); 1065 $value = $this->getGedcomValue($tag, $level, $this->gedrec); 1066 switch (end($tags)) { 1067 case 'DATE': 1068 $tmp = new Date($value); 1069 $value = strip_tags($tmp->display()); 1070 break; 1071 case 'PLAC': 1072 $tmp = new Place($value, $this->tree); 1073 $value = $tmp->shortName(); 1074 break; 1075 } 1076 if ($useBreak === '1') { 1077 // Insert <br> when multiple dates exist. 1078 // This works around a TCPDF bug that incorrectly wraps RTL dates on LTR pages 1079 $value = str_replace('(', '<br>(', $value); 1080 $value = str_replace('<span dir="ltr"><br>', '<br><span dir="ltr">', $value); 1081 $value = str_replace('<span dir="rtl"><br>', '<br><span dir="rtl">', $value); 1082 if (substr($value, 0, 4) === '<br>') { 1083 $value = substr($value, 4); 1084 } 1085 } 1086 $tmp = explode(':', $tag); 1087 if (in_array(end($tmp), ['NOTE', 'TEXT'], true)) { 1088 if ($this->tree->getPreference('FORMAT_TEXT') === 'markdown') { 1089 $value = strip_tags(Registry::markdownFactory()->markdown($value, $this->tree)); 1090 } else { 1091 $value = strip_tags(Registry::markdownFactory()->autolink($value, $this->tree)); 1092 } 1093 } 1094 1095 if (!empty($attrs['truncate'])) { 1096 $value = Str::limit($value, (int) $attrs['truncate'], I18N::translate('…')); 1097 } 1098 $this->current_element->addText($value); 1099 } 1100 } 1101 } 1102 1103 /** 1104 * Handle <repeatTag> 1105 * 1106 * @param array<string> $attrs 1107 * 1108 * @return void 1109 */ 1110 protected function repeatTagStartHandler(array $attrs): void 1111 { 1112 $this->process_repeats++; 1113 if ($this->process_repeats > 1) { 1114 return; 1115 } 1116 1117 $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes]; 1118 $this->repeats = []; 1119 $this->repeat_bytes = xml_get_current_line_number($this->parser); 1120 1121 $tag = $attrs['tag'] ?? ''; 1122 if (!empty($tag)) { 1123 if ($tag === '@desc') { 1124 $value = $this->desc; 1125 $value = trim($value); 1126 $this->current_element->addText($value); 1127 } else { 1128 $tag = str_replace('@fact', $this->fact, $tag); 1129 $tags = explode(':', $tag); 1130 $level = (int) explode(' ', trim($this->gedrec))[0]; 1131 if ($level === 0) { 1132 $level++; 1133 } 1134 $subrec = $this->gedrec; 1135 $t = $tag; 1136 $count = count($tags); 1137 $i = 0; 1138 while ($i < $count) { 1139 $t = $tags[$i]; 1140 if (!empty($t)) { 1141 if ($i < ($count - 1)) { 1142 $subrec = self::getSubRecord($level, "$level $t", $subrec); 1143 if (empty($subrec)) { 1144 $level--; 1145 $subrec = self::getSubRecord($level, "@ $t", $this->gedrec); 1146 if (empty($subrec)) { 1147 return; 1148 } 1149 } 1150 } 1151 $level++; 1152 } 1153 $i++; 1154 } 1155 $level--; 1156 $count = preg_match_all("/$level $t(.*)/", $subrec, $match, PREG_SET_ORDER); 1157 $i = 0; 1158 while ($i < $count) { 1159 $i++; 1160 // Privacy check - is this a link, and are we allowed to view the linked object? 1161 $subrecord = self::getSubRecord($level, "$level $t", $subrec, $i); 1162 if (preg_match('/^\d ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@/', $subrecord, $xref_match)) { 1163 $linked_object = Registry::gedcomRecordFactory()->make($xref_match[1], $this->tree); 1164 if ($linked_object && !$linked_object->canShow()) { 1165 continue; 1166 } 1167 } 1168 $this->repeats[] = $subrecord; 1169 } 1170 } 1171 } 1172 } 1173 1174 /** 1175 * Handle </repeatTag> 1176 * 1177 * @return void 1178 */ 1179 protected function repeatTagEndHandler(): void 1180 { 1181 $this->process_repeats--; 1182 if ($this->process_repeats > 0) { 1183 return; 1184 } 1185 1186 // Check if there is anything to repeat 1187 if (count($this->repeats) > 0) { 1188 // No need to load them if not used... 1189 1190 $lineoffset = 0; 1191 foreach ($this->repeats_stack as $rep) { 1192 $lineoffset += $rep[1]; 1193 } 1194 //-- read the xml from the file 1195 $lines = file($this->report); 1196 while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<RepeatTag')) { 1197 $lineoffset--; 1198 } 1199 $lineoffset++; 1200 $reportxml = "<tempdoc>\n"; 1201 $line_nr = $lineoffset + $this->repeat_bytes; 1202 // RepeatTag Level counter 1203 $count = 1; 1204 while (0 < $count) { 1205 if (str_contains($lines[$line_nr], '<RepeatTag')) { 1206 $count++; 1207 } elseif (str_contains($lines[$line_nr], '</RepeatTag')) { 1208 $count--; 1209 } 1210 if (0 < $count) { 1211 $reportxml .= $lines[$line_nr]; 1212 } 1213 $line_nr++; 1214 } 1215 // No need to drag this 1216 unset($lines); 1217 $reportxml .= "</tempdoc>\n"; 1218 // Save original values 1219 $this->parser_stack[] = $this->parser; 1220 $oldgedrec = $this->gedrec; 1221 foreach ($this->repeats as $gedrec) { 1222 $this->gedrec = $gedrec; 1223 $repeat_parser = xml_parser_create(); 1224 $this->parser = $repeat_parser; 1225 xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false); 1226 1227 xml_set_element_handler( 1228 $repeat_parser, 1229 function ($parser, string $name, array $attrs): void { 1230 $this->startElement($parser, $name, $attrs); 1231 }, 1232 function ($parser, string $name): void { 1233 $this->endElement($parser, $name); 1234 } 1235 ); 1236 1237 xml_set_character_data_handler( 1238 $repeat_parser, 1239 function ($parser, string $data): void { 1240 $this->characterData($parser, $data); 1241 } 1242 ); 1243 1244 if (!xml_parse($repeat_parser, $reportxml, true)) { 1245 throw new DomainException(sprintf( 1246 'RepeatTagEHandler XML error: %s at line %d', 1247 xml_error_string(xml_get_error_code($repeat_parser)), 1248 xml_get_current_line_number($repeat_parser) 1249 )); 1250 } 1251 xml_parser_free($repeat_parser); 1252 } 1253 // Restore original values 1254 $this->gedrec = $oldgedrec; 1255 $this->parser = array_pop($this->parser_stack); 1256 } 1257 [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack); 1258 } 1259 1260 /** 1261 * Variable lookup 1262 * Retrieve predefined variables : 1263 * @ desc GEDCOM fact description, example: 1264 * 1 EVEN This is a description 1265 * @ fact GEDCOM fact tag, such as BIRT, DEAT etc. 1266 * $ I18N::translate('....') 1267 * $ language_settings[] 1268 * 1269 * @param array<string> $attrs an array of key value pairs for the attributes 1270 * 1271 * @return void 1272 */ 1273 protected function varStartHandler(array $attrs): void 1274 { 1275 if (empty($attrs['var'])) { 1276 throw new DomainException('REPORT ERROR var: The attribute "var=" is missing or not set in the XML file on line: ' . xml_get_current_line_number($this->parser)); 1277 } 1278 1279 $var = $attrs['var']; 1280 // SetVar element preset variables 1281 if (!empty($this->vars[$var]['id'])) { 1282 $var = $this->vars[$var]['id']; 1283 } else { 1284 $tfact = $this->fact; 1285 if (($this->fact === 'EVEN' || $this->fact === 'FACT') && $this->type !== '') { 1286 // Use : 1287 // n TYPE This text if string 1288 $tfact = $this->type; 1289 } else { 1290 foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) { 1291 $element = Registry::elementFactory()->make($record_type . ':' . $this->fact); 1292 1293 if (!$element instanceof UnknownElement) { 1294 $tfact = $element->label(); 1295 break; 1296 } 1297 } 1298 } 1299 1300 $var = strtr($var, ['@desc' => $this->desc, '@fact' => $tfact]); 1301 1302 if (preg_match('/^I18N::number\((.+)\)$/', $var, $match)) { 1303 $var = I18N::number((int) $match[1]); 1304 } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $var, $match)) { 1305 $var = I18N::translate($match[1]); 1306 } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $var, $match)) { 1307 $var = I18N::translateContext($match[1], $match[2]); 1308 } 1309 } 1310 // Check if variable is set as a date and reformat the date 1311 if (isset($attrs['date'])) { 1312 if ($attrs['date'] === '1') { 1313 $g = new Date($var); 1314 $var = $g->display(); 1315 } 1316 } 1317 $this->current_element->addText($var); 1318 $this->text = $var; // Used for title/descriptio 1319 } 1320 1321 /** 1322 * Handle <facts> 1323 * 1324 * @param array<string> $attrs 1325 * 1326 * @return void 1327 */ 1328 protected function factsStartHandler(array $attrs): void 1329 { 1330 $this->process_repeats++; 1331 if ($this->process_repeats > 1) { 1332 return; 1333 } 1334 1335 $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes]; 1336 $this->repeats = []; 1337 $this->repeat_bytes = xml_get_current_line_number($this->parser); 1338 1339 $id = ''; 1340 $match = []; 1341 if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) { 1342 $id = $match[1]; 1343 } 1344 $tag = ''; 1345 if (isset($attrs['ignore'])) { 1346 $tag .= $attrs['ignore']; 1347 } 1348 if (preg_match('/\$(.+)/', $tag, $match)) { 1349 $tag = $this->vars[$match[1]]['id']; 1350 } 1351 1352 $record = Registry::gedcomRecordFactory()->make($id, $this->tree); 1353 if (empty($attrs['diff']) && !empty($id)) { 1354 $facts = $record->facts([], true); 1355 $this->repeats = []; 1356 $nonfacts = explode(',', $tag); 1357 foreach ($facts as $fact) { 1358 $tag = explode(':', $fact->tag())[1]; 1359 1360 if (!in_array($tag, $nonfacts, true)) { 1361 $this->repeats[] = $fact->gedcom(); 1362 } 1363 } 1364 } else { 1365 foreach ($record->facts() as $fact) { 1366 if (($fact->isPendingAddition() || $fact->isPendingDeletion()) && !str_ends_with($fact->tag(), ':CHAN')) { 1367 $this->repeats[] = $fact->gedcom(); 1368 } 1369 } 1370 } 1371 } 1372 1373 /** 1374 * Handle </facts> 1375 * 1376 * @return void 1377 */ 1378 protected function factsEndHandler(): void 1379 { 1380 $this->process_repeats--; 1381 if ($this->process_repeats > 0) { 1382 return; 1383 } 1384 1385 // Check if there is anything to repeat 1386 if (count($this->repeats) > 0) { 1387 $line = xml_get_current_line_number($this->parser) - 1; 1388 $lineoffset = 0; 1389 foreach ($this->repeats_stack as $rep) { 1390 $lineoffset += $rep[1]; 1391 } 1392 1393 //-- read the xml from the file 1394 $lines = file($this->report); 1395 while ($lineoffset + $this->repeat_bytes > 0 && !str_contains($lines[$lineoffset + $this->repeat_bytes], '<Facts ')) { 1396 $lineoffset--; 1397 } 1398 $lineoffset++; 1399 $reportxml = "<tempdoc>\n"; 1400 $i = $line + $lineoffset; 1401 $line_nr = $this->repeat_bytes + $lineoffset; 1402 while ($line_nr < $i) { 1403 $reportxml .= $lines[$line_nr]; 1404 $line_nr++; 1405 } 1406 // No need to drag this 1407 unset($lines); 1408 $reportxml .= "</tempdoc>\n"; 1409 // Save original values 1410 $this->parser_stack[] = $this->parser; 1411 $oldgedrec = $this->gedrec; 1412 $count = count($this->repeats); 1413 $i = 0; 1414 while ($i < $count) { 1415 $this->gedrec = $this->repeats[$i]; 1416 $this->fact = ''; 1417 $this->desc = ''; 1418 if (preg_match('/1 (\w+)(.*)/', $this->gedrec, $match)) { 1419 $this->fact = $match[1]; 1420 if ($this->fact === 'EVEN' || $this->fact === 'FACT') { 1421 $tmatch = []; 1422 if (preg_match('/2 TYPE (.+)/', $this->gedrec, $tmatch)) { 1423 $this->type = trim($tmatch[1]); 1424 } else { 1425 $this->type = ' '; 1426 } 1427 } 1428 $this->desc = trim($match[2]); 1429 $this->desc .= self::getCont(2, $this->gedrec); 1430 } 1431 $repeat_parser = xml_parser_create(); 1432 $this->parser = $repeat_parser; 1433 xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false); 1434 1435 xml_set_element_handler( 1436 $repeat_parser, 1437 function ($parser, string $name, array $attrs): void { 1438 $this->startElement($parser, $name, $attrs); 1439 }, 1440 function ($parser, string $name): void { 1441 $this->endElement($parser, $name); 1442 } 1443 ); 1444 1445 xml_set_character_data_handler( 1446 $repeat_parser, 1447 function ($parser, string $data): void { 1448 $this->characterData($parser, $data); 1449 } 1450 ); 1451 1452 if (!xml_parse($repeat_parser, $reportxml, true)) { 1453 throw new DomainException(sprintf( 1454 'FactsEHandler XML error: %s at line %d', 1455 xml_error_string(xml_get_error_code($repeat_parser)), 1456 xml_get_current_line_number($repeat_parser) 1457 )); 1458 } 1459 xml_parser_free($repeat_parser); 1460 $i++; 1461 } 1462 // Restore original values 1463 $this->parser = array_pop($this->parser_stack); 1464 $this->gedrec = $oldgedrec; 1465 } 1466 [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack); 1467 } 1468 1469 /** 1470 * Setting upp or changing variables in the XML 1471 * The XML variable name and value is stored in $this->vars 1472 * 1473 * @param array<string> $attrs an array of key value pairs for the attributes 1474 * 1475 * @return void 1476 */ 1477 protected function setVarStartHandler(array $attrs): void 1478 { 1479 if (empty($attrs['name'])) { 1480 throw new DomainException('REPORT ERROR var: The attribute "name" is missing or not set in the XML file'); 1481 } 1482 1483 $name = $attrs['name']; 1484 $value = $attrs['value']; 1485 $match = []; 1486 // Current GEDCOM record strings 1487 if ($value === '@ID') { 1488 if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) { 1489 $value = $match[1]; 1490 } 1491 } elseif ($value === '@fact') { 1492 $value = $this->fact; 1493 } elseif ($value === '@desc') { 1494 $value = $this->desc; 1495 } elseif ($value === '@generation') { 1496 $value = (string) $this->generation; 1497 } elseif (preg_match("/@(\w+)/", $value, $match)) { 1498 $gmatch = []; 1499 if (preg_match("/\d $match[1] (.+)/", $this->gedrec, $gmatch)) { 1500 $value = str_replace('@', '', trim($gmatch[1])); 1501 } 1502 } 1503 if (preg_match("/\\$(\w+)/", $name, $match)) { 1504 $name = $this->vars["'" . $match[1] . "'"]['id']; 1505 } 1506 $count = preg_match_all("/\\$(\w+)/", $value, $match, PREG_SET_ORDER); 1507 $i = 0; 1508 while ($i < $count) { 1509 $t = $this->vars[$match[$i][1]]['id']; 1510 $value = preg_replace('/\$' . $match[$i][1] . '/', $t, $value, 1); 1511 $i++; 1512 } 1513 if (preg_match('/^I18N::number\((.+)\)$/', $value, $match)) { 1514 $value = I18N::number((int) $match[1]); 1515 } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $value, $match)) { 1516 $value = I18N::translate($match[1]); 1517 } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $value, $match)) { 1518 $value = I18N::translateContext($match[1], $match[2]); 1519 } 1520 1521 // Arithmetic functions 1522 if (preg_match("/(\d+)\s*([-+*\/])\s*(\d+)/", $value, $match)) { 1523 // Create an expression language with the functions used by our reports. 1524 $expression_provider = new ReportExpressionLanguageProvider(); 1525 $expression_cache = new NullAdapter(); 1526 $expression_language = new ExpressionLanguage($expression_cache, [$expression_provider]); 1527 1528 $value = (string) $expression_language->evaluate($value); 1529 } 1530 1531 if (str_contains($value, '@')) { 1532 $value = ''; 1533 } 1534 $this->vars[$name]['id'] = $value; 1535 } 1536 1537 /** 1538 * Handle <if> 1539 * 1540 * @param array<string> $attrs 1541 * 1542 * @return void 1543 */ 1544 protected function ifStartHandler(array $attrs): void 1545 { 1546 if ($this->process_ifs > 0) { 1547 $this->process_ifs++; 1548 1549 return; 1550 } 1551 1552 $condition = $attrs['condition']; 1553 $condition = $this->substituteVars($condition, true); 1554 $condition = str_replace([ 1555 ' LT ', 1556 ' GT ', 1557 ], [ 1558 '<', 1559 '>', 1560 ], $condition); 1561 // Replace the first occurrence only once of @fact:DATE or in any other combinations to the current fact, such as BIRT 1562 $condition = str_replace('@fact:', $this->fact . ':', $condition); 1563 $match = []; 1564 $count = preg_match_all("/@([\w:.]+)/", $condition, $match, PREG_SET_ORDER); 1565 $i = 0; 1566 while ($i < $count) { 1567 $id = $match[$i][1]; 1568 $value = '""'; 1569 if ($id === 'ID') { 1570 if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) { 1571 $value = "'" . $match[1] . "'"; 1572 } 1573 } elseif ($id === 'fact') { 1574 $value = '"' . $this->fact . '"'; 1575 } elseif ($id === 'desc') { 1576 $value = '"' . addslashes($this->desc) . '"'; 1577 } elseif ($id === 'generation') { 1578 $value = '"' . $this->generation . '"'; 1579 } else { 1580 $level = (int) explode(' ', trim($this->gedrec))[0]; 1581 if ($level === 0) { 1582 $level++; 1583 } 1584 $value = $this->getGedcomValue($id, $level, $this->gedrec); 1585 if (empty($value)) { 1586 $level++; 1587 $value = $this->getGedcomValue($id, $level, $this->gedrec); 1588 } 1589 $value = preg_replace('/^@(' . Gedcom::REGEX_XREF . ')@$/', '$1', $value); 1590 $value = '"' . addslashes($value) . '"'; 1591 } 1592 $condition = str_replace("@$id", $value, $condition); 1593 $i++; 1594 } 1595 1596 // Create an expression language with the functions used by our reports. 1597 $expression_provider = new ReportExpressionLanguageProvider(); 1598 $expression_cache = new NullAdapter(); 1599 $expression_language = new ExpressionLanguage($expression_cache, [$expression_provider]); 1600 1601 $ret = $expression_language->evaluate($condition); 1602 1603 if (!$ret) { 1604 $this->process_ifs++; 1605 } 1606 } 1607 1608 /** 1609 * Handle </if> 1610 * 1611 * @return void 1612 */ 1613 protected function ifEndHandler(): void 1614 { 1615 if ($this->process_ifs > 0) { 1616 $this->process_ifs--; 1617 } 1618 } 1619 1620 /** 1621 * Handle <footnote> 1622 * Collect the Footnote links 1623 * GEDCOM Records that are protected by Privacy setting will be ignored 1624 * 1625 * @param array<string> $attrs 1626 * 1627 * @return void 1628 */ 1629 protected function footnoteStartHandler(array $attrs): void 1630 { 1631 $id = ''; 1632 if (preg_match('/[0-9] (.+) @(.+)@/', $this->gedrec, $match)) { 1633 $id = $match[2]; 1634 } 1635 $record = Registry::gedcomRecordFactory()->make($id, $this->tree); 1636 if ($record && $record->canShow()) { 1637 $this->print_data_stack[] = $this->print_data; 1638 $this->print_data = true; 1639 $style = ''; 1640 if (!empty($attrs['style'])) { 1641 $style = $attrs['style']; 1642 } 1643 $this->footnote_element = $this->current_element; 1644 $this->current_element = $this->report_root->createFootnote($style); 1645 } else { 1646 $this->print_data = false; 1647 $this->process_footnote = false; 1648 } 1649 } 1650 1651 /** 1652 * Handle </footnote> 1653 * Print the collected Footnote data 1654 * 1655 * @return void 1656 */ 1657 protected function footnoteEndHandler(): void 1658 { 1659 if ($this->process_footnote) { 1660 $this->print_data = array_pop($this->print_data_stack); 1661 $temp = trim($this->current_element->getValue()); 1662 if (strlen($temp) > 3) { 1663 $this->wt_report->addElement($this->current_element); 1664 } 1665 $this->current_element = $this->footnote_element; 1666 } else { 1667 $this->process_footnote = true; 1668 } 1669 } 1670 1671 /** 1672 * Handle <footnoteTexts /> 1673 * 1674 * @return void 1675 */ 1676 protected function footnoteTextsStartHandler(): void 1677 { 1678 $temp = 'footnotetexts'; 1679 $this->wt_report->addElement($temp); 1680 } 1681 1682 /** 1683 * XML element Forced line break handler - HTML code 1684 * 1685 * @return void 1686 */ 1687 protected function brStartHandler(): void 1688 { 1689 if ($this->print_data && $this->process_gedcoms === 0) { 1690 $this->current_element->addText('<br>'); 1691 } 1692 } 1693 1694 /** 1695 * Handle <sp /> 1696 * Forced space 1697 * 1698 * @return void 1699 */ 1700 protected function spStartHandler(): void 1701 { 1702 if ($this->print_data && $this->process_gedcoms === 0) { 1703 $this->current_element->addText(' '); 1704 } 1705 } 1706 1707 /** 1708 * Handle <highlightedImage /> 1709 * 1710 * @param array<string> $attrs 1711 * 1712 * @return void 1713 */ 1714 protected function highlightedImageStartHandler(array $attrs): void 1715 { 1716 $id = ''; 1717 if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) { 1718 $id = $match[1]; 1719 } 1720 1721 // Position the top corner of this box on the page 1722 $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION); 1723 1724 // Position the left corner of this box on the page 1725 $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION); 1726 1727 // string Align the image in left, center, right (or empty to use x/y position). 1728 $align = $attrs['align'] ?? ''; 1729 1730 // string Next Line should be T:next to the image, N:next line 1731 $ln = $attrs['ln'] ?? 'T'; 1732 1733 // Width, height (or both). 1734 $width = (float) ($attrs['width'] ?? 0.0); 1735 $height = (float) ($attrs['height'] ?? 0.0); 1736 1737 $person = Registry::individualFactory()->make($id, $this->tree); 1738 $media_file = $person->findHighlightedMediaFile(); 1739 1740 if ($media_file instanceof MediaFile && $media_file->fileExists($this->data_filesystem)) { 1741 $image = imagecreatefromstring($media_file->fileContents($this->data_filesystem)); 1742 $attributes = [imagesx($image), imagesy($image)]; 1743 1744 if ($width > 0 && $height == 0) { 1745 $perc = $width / $attributes[0]; 1746 $height = round($attributes[1] * $perc); 1747 } elseif ($height > 0 && $width == 0) { 1748 $perc = $height / $attributes[1]; 1749 $width = round($attributes[0] * $perc); 1750 } else { 1751 $width = $attributes[0]; 1752 $height = $attributes[1]; 1753 } 1754 $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln, $this->data_filesystem); 1755 $this->wt_report->addElement($image); 1756 } 1757 } 1758 1759 /** 1760 * Handle <image/> 1761 * 1762 * @param array<string> $attrs 1763 * 1764 * @return void 1765 */ 1766 protected function imageStartHandler(array $attrs): void 1767 { 1768 // Position the top corner of this box on the page. the default is the current position 1769 $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION); 1770 1771 // mixed Position the left corner of this box on the page. the default is the current position 1772 $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION); 1773 1774 // string Align the image in left, center, right (or empty to use x/y position). 1775 $align = $attrs['align'] ?? ''; 1776 1777 // string Next Line should be T:next to the image, N:next line 1778 $ln = $attrs['ln'] ?? 'T'; 1779 1780 // Width, height (or both). 1781 $width = (float) ($attrs['width'] ?? 0.0); 1782 $height = (float) ($attrs['height'] ?? 0.0); 1783 1784 $file = $attrs['file'] ?? ''; 1785 1786 if ($file === '@FILE') { 1787 $match = []; 1788 if (preg_match("/\d OBJE @(.+)@/", $this->gedrec, $match)) { 1789 $mediaobject = Registry::mediaFactory()->make($match[1], $this->tree); 1790 $media_file = $mediaobject->firstImageFile(); 1791 1792 if ($media_file instanceof MediaFile && $media_file->fileExists($this->data_filesystem)) { 1793 $image = imagecreatefromstring($media_file->fileContents($this->data_filesystem)); 1794 $attributes = [imagesx($image), imagesy($image)]; 1795 1796 if ($width > 0 && $height == 0) { 1797 $perc = $width / $attributes[0]; 1798 $height = round($attributes[1] * $perc); 1799 } elseif ($height > 0 && $width == 0) { 1800 $perc = $height / $attributes[1]; 1801 $width = round($attributes[0] * $perc); 1802 } else { 1803 $width = $attributes[0]; 1804 $height = $attributes[1]; 1805 } 1806 $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln, $this->data_filesystem); 1807 $this->wt_report->addElement($image); 1808 } 1809 } 1810 } else { 1811 if (file_exists($file) && preg_match('/(jpg|jpeg|png|gif)$/i', $file)) { 1812 $size = getimagesize($file); 1813 if ($width > 0 && $height == 0) { 1814 $perc = $width / $size[0]; 1815 $height = round($size[1] * $perc); 1816 } elseif ($height > 0 && $width == 0) { 1817 $perc = $height / $size[1]; 1818 $width = round($size[0] * $perc); 1819 } else { 1820 $width = $size[0]; 1821 $height = $size[1]; 1822 } 1823 $image = $this->report_root->createImage($file, $left, $top, $width, $height, $align, $ln); 1824 $this->wt_report->addElement($image); 1825 } 1826 } 1827 } 1828 1829 /** 1830 * Handle <line> 1831 * 1832 * @param array<string> $attrs 1833 * 1834 * @return void 1835 */ 1836 protected function lineStartHandler(array $attrs): void 1837 { 1838 // Start horizontal position, current position (default) 1839 $x1 = ReportBaseElement::CURRENT_POSITION; 1840 if (isset($attrs['x1'])) { 1841 if ($attrs['x1'] === '0') { 1842 $x1 = 0; 1843 } elseif ($attrs['x1'] === '.') { 1844 $x1 = ReportBaseElement::CURRENT_POSITION; 1845 } elseif (!empty($attrs['x1'])) { 1846 $x1 = (float) $attrs['x1']; 1847 } 1848 } 1849 // Start vertical position, current position (default) 1850 $y1 = ReportBaseElement::CURRENT_POSITION; 1851 if (isset($attrs['y1'])) { 1852 if ($attrs['y1'] === '0') { 1853 $y1 = 0; 1854 } elseif ($attrs['y1'] === '.') { 1855 $y1 = ReportBaseElement::CURRENT_POSITION; 1856 } elseif (!empty($attrs['y1'])) { 1857 $y1 = (float) $attrs['y1']; 1858 } 1859 } 1860 // End horizontal position, maximum width (default) 1861 $x2 = ReportBaseElement::CURRENT_POSITION; 1862 if (isset($attrs['x2'])) { 1863 if ($attrs['x2'] === '0') { 1864 $x2 = 0; 1865 } elseif ($attrs['x2'] === '.') { 1866 $x2 = ReportBaseElement::CURRENT_POSITION; 1867 } elseif (!empty($attrs['x2'])) { 1868 $x2 = (float) $attrs['x2']; 1869 } 1870 } 1871 // End vertical position 1872 $y2 = ReportBaseElement::CURRENT_POSITION; 1873 if (isset($attrs['y2'])) { 1874 if ($attrs['y2'] === '0') { 1875 $y2 = 0; 1876 } elseif ($attrs['y2'] === '.') { 1877 $y2 = ReportBaseElement::CURRENT_POSITION; 1878 } elseif (!empty($attrs['y2'])) { 1879 $y2 = (float) $attrs['y2']; 1880 } 1881 } 1882 1883 $line = $this->report_root->createLine($x1, $y1, $x2, $y2); 1884 $this->wt_report->addElement($line); 1885 } 1886 1887 /** 1888 * Handle <list> 1889 * 1890 * @param array<string> $attrs 1891 * 1892 * @return void 1893 */ 1894 protected function listStartHandler(array $attrs): void 1895 { 1896 $this->process_repeats++; 1897 if ($this->process_repeats > 1) { 1898 return; 1899 } 1900 1901 $match = []; 1902 if (isset($attrs['sortby'])) { 1903 $sortby = $attrs['sortby']; 1904 if (preg_match("/\\$(\w+)/", $sortby, $match)) { 1905 $sortby = $this->vars[$match[1]]['id']; 1906 $sortby = trim($sortby); 1907 } 1908 } else { 1909 $sortby = 'NAME'; 1910 } 1911 1912 $listname = $attrs['list'] ?? 'individual'; 1913 1914 // Some filters/sorts can be applied using SQL, while others require PHP 1915 switch ($listname) { 1916 case 'pending': 1917 $this->list = DB::table('change') 1918 ->whereIn('change_id', function (Builder $query): void { 1919 $query->select(new Expression('MAX(change_id)')) 1920 ->from('change') 1921 ->where('gedcom_id', '=', $this->tree->id()) 1922 ->where('status', '=', 'pending') 1923 ->groupBy(['xref']); 1924 }) 1925 ->get() 1926 ->map(fn (object $row): ?GedcomRecord => Registry::gedcomRecordFactory()->make($row->xref, $this->tree, $row->new_gedcom ?: $row->old_gedcom)) 1927 ->filter() 1928 ->all(); 1929 break; 1930 1931 case 'individual': 1932 $query = DB::table('individuals') 1933 ->where('i_file', '=', $this->tree->id()) 1934 ->select(['i_id AS xref', 'i_gedcom AS gedcom']) 1935 ->distinct(); 1936 1937 foreach ($attrs as $attr => $value) { 1938 if (str_starts_with($attr, 'filter') && $value !== '') { 1939 $value = $this->substituteVars($value, false); 1940 // Convert the various filters into SQL 1941 if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) { 1942 $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void { 1943 $join 1944 ->on($attr . '.d_gid', '=', 'i_id') 1945 ->on($attr . '.d_file', '=', 'i_file'); 1946 }); 1947 1948 $query->where($attr . '.d_fact', '=', $match[1]); 1949 1950 $date = new Date($match[3]); 1951 1952 if ($match[2] === 'LTE') { 1953 $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay()); 1954 } else { 1955 $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay()); 1956 } 1957 1958 // This filter has been fully processed 1959 unset($attrs[$attr]); 1960 } elseif (preg_match('/^NAME CONTAINS (.+)$/', $value, $match)) { 1961 $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void { 1962 $join 1963 ->on($attr . '.n_id', '=', 'i_id') 1964 ->on($attr . '.n_file', '=', 'i_file'); 1965 }); 1966 // Search the DB only if there is any name supplied 1967 $names = explode(' ', $match[1]); 1968 foreach ($names as $n => $name) { 1969 $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%'); 1970 } 1971 1972 // This filter has been fully processed 1973 unset($attrs[$attr]); 1974 } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) { 1975 // Convert newline escape sequences to actual new lines 1976 $match[1] = str_replace('\n', "\n", $match[1]); 1977 1978 $query->where('i_gedcom', 'LIKE', $match[1]); 1979 1980 // This filter has been fully processed 1981 unset($attrs[$attr]); 1982 } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) { 1983 // Don't unset this filter. This is just initial filtering for performance 1984 $query 1985 ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void { 1986 $join 1987 ->on($attr . 'a.pl_file', '=', 'i_file') 1988 ->on($attr . 'a.pl_gid', '=', 'i_id'); 1989 }) 1990 ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void { 1991 $join 1992 ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file') 1993 ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id'); 1994 }) 1995 ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%'); 1996 } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) { 1997 // Don't unset this filter. This is just initial filtering for performance 1998 $match[3] = strtr($match[3], ['\\' => '\\\\', '%' => '\\%', '_' => '\\_', ' ' => '%']); 1999 $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%'; 2000 $query->where('i_gedcom', 'LIKE', $like); 2001 } elseif (preg_match('/^(\w+) CONTAINS (.*)$/', $value, $match)) { 2002 // Don't unset this filter. This is just initial filtering for performance 2003 $match[2] = strtr($match[2], ['\\' => '\\\\', '%' => '\\%', '_' => '\\_', ' ' => '%']); 2004 $like = "%\n1 " . $match[1] . '%' . $match[2] . '%'; 2005 $query->where('i_gedcom', 'LIKE', $like); 2006 } 2007 } 2008 } 2009 2010 $this->list = []; 2011 2012 foreach ($query->get() as $row) { 2013 $this->list[$row->xref] = Registry::individualFactory()->make($row->xref, $this->tree, $row->gedcom); 2014 } 2015 break; 2016 2017 case 'family': 2018 $query = DB::table('families') 2019 ->where('f_file', '=', $this->tree->id()) 2020 ->select(['f_id AS xref', 'f_gedcom AS gedcom']) 2021 ->distinct(); 2022 2023 foreach ($attrs as $attr => $value) { 2024 if (str_starts_with($attr, 'filter') && $value !== '') { 2025 $value = $this->substituteVars($value, false); 2026 // Convert the various filters into SQL 2027 if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) { 2028 $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void { 2029 $join 2030 ->on($attr . '.d_gid', '=', 'f_id') 2031 ->on($attr . '.d_file', '=', 'f_file'); 2032 }); 2033 2034 $query->where($attr . '.d_fact', '=', $match[1]); 2035 2036 $date = new Date($match[3]); 2037 2038 if ($match[2] === 'LTE') { 2039 $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay()); 2040 } else { 2041 $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay()); 2042 } 2043 2044 // This filter has been fully processed 2045 unset($attrs[$attr]); 2046 } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) { 2047 // Convert newline escape sequences to actual new lines 2048 $match[1] = str_replace('\n', "\n", $match[1]); 2049 2050 $query->where('f_gedcom', 'LIKE', $match[1]); 2051 2052 // This filter has been fully processed 2053 unset($attrs[$attr]); 2054 } elseif (preg_match('/^NAME CONTAINS (.*)$/', $value, $match)) { 2055 if ($sortby === 'NAME' || $match[1] !== '') { 2056 $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void { 2057 $join 2058 ->on($attr . '.n_file', '=', 'f_file') 2059 ->where(static function (Builder $query): void { 2060 $query 2061 ->whereColumn('n_id', '=', 'f_husb') 2062 ->orWhereColumn('n_id', '=', 'f_wife'); 2063 }); 2064 }); 2065 // Search the DB only if there is any name supplied 2066 if ($match[1] != '') { 2067 $names = explode(' ', $match[1]); 2068 foreach ($names as $n => $name) { 2069 $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%'); 2070 } 2071 } 2072 } 2073 2074 // This filter has been fully processed 2075 unset($attrs[$attr]); 2076 } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) { 2077 // Don't unset this filter. This is just initial filtering for performance 2078 $query 2079 ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void { 2080 $join 2081 ->on($attr . 'a.pl_file', '=', 'f_file') 2082 ->on($attr . 'a.pl_gid', '=', 'f_id'); 2083 }) 2084 ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void { 2085 $join 2086 ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file') 2087 ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id'); 2088 }) 2089 ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%'); 2090 } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) { 2091 // Don't unset this filter. This is just initial filtering for performance 2092 $match[3] = strtr($match[3], ['\\' => '\\\\', '%' => '\\%', '_' => '\\_', ' ' => '%']); 2093 $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%'; 2094 $query->where('f_gedcom', 'LIKE', $like); 2095 } elseif (preg_match('/^(\w+) CONTAINS (.+)$/', $value, $match)) { 2096 // Don't unset this filter. This is just initial filtering for performance 2097 $match[2] = strtr($match[2], ['\\' => '\\\\', '%' => '\\%', '_' => '\\_', ' ' => '%']); 2098 $like = "%\n1 " . $match[1] . '%' . $match[2] . '%'; 2099 $query->where('f_gedcom', 'LIKE', $like); 2100 } 2101 } 2102 } 2103 2104 $this->list = []; 2105 2106 foreach ($query->get() as $row) { 2107 $this->list[$row->xref] = Registry::familyFactory()->make($row->xref, $this->tree, $row->gedcom); 2108 } 2109 break; 2110 2111 default: 2112 throw new DomainException('Invalid list name: ' . $listname); 2113 } 2114 2115 $filters = []; 2116 $filters2 = []; 2117 if (isset($attrs['filter1']) && count($this->list) > 0) { 2118 foreach ($attrs as $key => $value) { 2119 if (preg_match("/filter(\d)/", $key)) { 2120 $condition = $value; 2121 if (preg_match("/@(\w+)/", $condition, $match)) { 2122 $id = $match[1]; 2123 $value = "''"; 2124 if ($id === 'ID') { 2125 if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) { 2126 $value = "'" . $match[1] . "'"; 2127 } 2128 } elseif ($id === 'fact') { 2129 $value = "'" . $this->fact . "'"; 2130 } elseif ($id === 'desc') { 2131 $value = "'" . $this->desc . "'"; 2132 } else { 2133 if (preg_match("/\d $id (.+)/", $this->gedrec, $match)) { 2134 $value = "'" . str_replace('@', '', trim($match[1])) . "'"; 2135 } 2136 } 2137 $condition = preg_replace("/@$id/", $value, $condition); 2138 } 2139 //-- handle regular expressions 2140 if (preg_match("/([A-Z:]+)\s*([^\s]+)\s*(.+)/", $condition, $match)) { 2141 $tag = trim($match[1]); 2142 $expr = trim($match[2]); 2143 $val = trim($match[3]); 2144 if (preg_match("/\\$(\w+)/", $val, $match)) { 2145 $val = $this->vars[$match[1]]['id']; 2146 $val = trim($val); 2147 } 2148 if ($val) { 2149 $searchstr = ''; 2150 $tags = explode(':', $tag); 2151 //-- only limit to a level number if we are specifically looking at a level 2152 if (count($tags) > 1) { 2153 $level = 1; 2154 $t = 'XXXX'; 2155 foreach ($tags as $t) { 2156 if (!empty($searchstr)) { 2157 $searchstr .= "[^\n]*(\n[2-9][^\n]*)*\n"; 2158 } 2159 //-- search for both EMAIL and _EMAIL... silly double gedcom standard 2160 if ($t === 'EMAIL' || $t === '_EMAIL') { 2161 $t = '_?EMAIL'; 2162 } 2163 $searchstr .= $level . ' ' . $t; 2164 $level++; 2165 } 2166 } else { 2167 if ($tag === 'EMAIL' || $tag === '_EMAIL') { 2168 $tag = '_?EMAIL'; 2169 } 2170 $t = $tag; 2171 $searchstr = '1 ' . $tag; 2172 } 2173 switch ($expr) { 2174 case 'CONTAINS': 2175 if ($t === 'PLAC') { 2176 $searchstr .= "[^\n]*[, ]*" . $val; 2177 } else { 2178 $searchstr .= "[^\n]*" . $val; 2179 } 2180 $filters[] = $searchstr; 2181 break; 2182 default: 2183 $filters2[] = [ 2184 'tag' => $tag, 2185 'expr' => $expr, 2186 'val' => $val, 2187 ]; 2188 break; 2189 } 2190 } 2191 } 2192 } 2193 } 2194 } 2195 //-- apply other filters to the list that could not be added to the search string 2196 if ($filters) { 2197 foreach ($this->list as $key => $record) { 2198 foreach ($filters as $filter) { 2199 if (!preg_match('/' . $filter . '/i', $record->privatizeGedcom(Auth::accessLevel($this->tree)))) { 2200 unset($this->list[$key]); 2201 break; 2202 } 2203 } 2204 } 2205 } 2206 if ($filters2) { 2207 $mylist = []; 2208 foreach ($this->list as $indi) { 2209 $key = $indi->xref(); 2210 $grec = $indi->privatizeGedcom(Auth::accessLevel($this->tree)); 2211 $keep = true; 2212 foreach ($filters2 as $filter) { 2213 if ($keep) { 2214 $tag = $filter['tag']; 2215 $expr = $filter['expr']; 2216 $val = $filter['val']; 2217 if ($val === "''") { 2218 $val = ''; 2219 } 2220 $tags = explode(':', $tag); 2221 $t = end($tags); 2222 $v = $this->getGedcomValue($tag, 1, $grec); 2223 //-- check for EMAIL and _EMAIL (silly double gedcom standard :P) 2224 if ($t === 'EMAIL' && empty($v)) { 2225 $tag = str_replace('EMAIL', '_EMAIL', $tag); 2226 $tags = explode(':', $tag); 2227 $t = end($tags); 2228 $v = self::getSubRecord(1, $tag, $grec); 2229 } 2230 2231 switch ($expr) { 2232 case 'GTE': 2233 if ($t === 'DATE') { 2234 $date1 = new Date($v); 2235 $date2 = new Date($val); 2236 $keep = (Date::compare($date1, $date2) >= 0); 2237 } elseif ($val >= $v) { 2238 $keep = true; 2239 } 2240 break; 2241 case 'LTE': 2242 if ($t === 'DATE') { 2243 $date1 = new Date($v); 2244 $date2 = new Date($val); 2245 $keep = (Date::compare($date1, $date2) <= 0); 2246 } elseif ($val >= $v) { 2247 $keep = true; 2248 } 2249 break; 2250 default: 2251 if ($v == $val) { 2252 $keep = true; 2253 } else { 2254 $keep = false; 2255 } 2256 break; 2257 } 2258 } 2259 } 2260 if ($keep) { 2261 $mylist[$key] = $indi; 2262 } 2263 } 2264 $this->list = $mylist; 2265 } 2266 2267 switch ($sortby) { 2268 case 'NAME': 2269 uasort($this->list, GedcomRecord::nameComparator()); 2270 break; 2271 case 'CHAN': 2272 uasort($this->list, GedcomRecord::lastChangeComparator()); 2273 break; 2274 case 'BIRT:DATE': 2275 uasort($this->list, Individual::birthDateComparator()); 2276 break; 2277 case 'DEAT:DATE': 2278 uasort($this->list, Individual::deathDateComparator()); 2279 break; 2280 case 'MARR:DATE': 2281 uasort($this->list, Family::marriageDateComparator()); 2282 break; 2283 default: 2284 // unsorted or already sorted by SQL 2285 break; 2286 } 2287 2288 $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes]; 2289 $this->repeat_bytes = xml_get_current_line_number($this->parser) + 1; 2290 } 2291 2292 /** 2293 * Handle </list> 2294 * 2295 * @return void 2296 */ 2297 protected function listEndHandler(): void 2298 { 2299 $this->process_repeats--; 2300 if ($this->process_repeats > 0) { 2301 return; 2302 } 2303 2304 // Check if there is any list 2305 if (count($this->list) > 0) { 2306 $lineoffset = 0; 2307 foreach ($this->repeats_stack as $rep) { 2308 $lineoffset += $rep[1]; 2309 } 2310 //-- read the xml from the file 2311 $lines = file($this->report); 2312 while ((!str_contains($lines[$lineoffset + $this->repeat_bytes], '<List')) && (($lineoffset + $this->repeat_bytes) > 0)) { 2313 $lineoffset--; 2314 } 2315 $lineoffset++; 2316 $reportxml = "<tempdoc>\n"; 2317 $line_nr = $lineoffset + $this->repeat_bytes; 2318 // List Level counter 2319 $count = 1; 2320 while (0 < $count) { 2321 if (str_contains($lines[$line_nr], '<List')) { 2322 $count++; 2323 } elseif (str_contains($lines[$line_nr], '</List')) { 2324 $count--; 2325 } 2326 if (0 < $count) { 2327 $reportxml .= $lines[$line_nr]; 2328 } 2329 $line_nr++; 2330 } 2331 // No need to drag this 2332 unset($lines); 2333 $reportxml .= '</tempdoc>'; 2334 // Save original values 2335 $this->parser_stack[] = $this->parser; 2336 $oldgedrec = $this->gedrec; 2337 2338 $this->list_total = count($this->list); 2339 $this->list_private = 0; 2340 foreach ($this->list as $record) { 2341 if ($record->canShow()) { 2342 $this->gedrec = $record->privatizeGedcom(Auth::accessLevel($record->tree())); 2343 //-- start the sax parser 2344 $repeat_parser = xml_parser_create(); 2345 $this->parser = $repeat_parser; 2346 xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false); 2347 2348 xml_set_element_handler( 2349 $repeat_parser, 2350 function ($parser, string $name, array $attrs): void { 2351 $this->startElement($parser, $name, $attrs); 2352 }, 2353 function ($parser, string $name): void { 2354 $this->endElement($parser, $name); 2355 } 2356 ); 2357 2358 xml_set_character_data_handler( 2359 $repeat_parser, 2360 function ($parser, string $data): void { 2361 $this->characterData($parser, $data); 2362 } 2363 ); 2364 2365 if (!xml_parse($repeat_parser, $reportxml, true)) { 2366 throw new DomainException(sprintf( 2367 'ListEHandler XML error: %s at line %d', 2368 xml_error_string(xml_get_error_code($repeat_parser)), 2369 xml_get_current_line_number($repeat_parser) 2370 )); 2371 } 2372 xml_parser_free($repeat_parser); 2373 } else { 2374 $this->list_private++; 2375 } 2376 } 2377 $this->list = []; 2378 $this->parser = array_pop($this->parser_stack); 2379 $this->gedrec = $oldgedrec; 2380 } 2381 [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack); 2382 } 2383 2384 /** 2385 * Handle <listTotal> 2386 * Prints the total number of records in a list 2387 * The total number is collected from <list> and <relatives> 2388 * 2389 * @return void 2390 */ 2391 protected function listTotalStartHandler(): void 2392 { 2393 if ($this->list_private == 0) { 2394 $this->current_element->addText((string) $this->list_total); 2395 } else { 2396 $this->current_element->addText(($this->list_total - $this->list_private) . ' / ' . $this->list_total); 2397 } 2398 } 2399 2400 /** 2401 * Handle <relatives> 2402 * 2403 * @param array<string> $attrs 2404 * 2405 * @return void 2406 */ 2407 protected function relativesStartHandler(array $attrs): void 2408 { 2409 $this->process_repeats++; 2410 if ($this->process_repeats > 1) { 2411 return; 2412 } 2413 2414 $sortby = $attrs['sortby'] ?? 'NAME'; 2415 2416 $match = []; 2417 if (preg_match("/\\$(\w+)/", $sortby, $match)) { 2418 $sortby = $this->vars[$match[1]]['id']; 2419 $sortby = trim($sortby); 2420 } 2421 2422 $maxgen = -1; 2423 if (isset($attrs['maxgen'])) { 2424 $maxgen = (int) $attrs['maxgen']; 2425 } 2426 2427 $group = $attrs['group'] ?? 'child-family'; 2428 2429 if (preg_match("/\\$(\w+)/", $group, $match)) { 2430 $group = $this->vars[$match[1]]['id']; 2431 $group = trim($group); 2432 } 2433 2434 $id = $attrs['id'] ?? ''; 2435 2436 if (preg_match("/\\$(\w+)/", $id, $match)) { 2437 $id = $this->vars[$match[1]]['id']; 2438 $id = trim($id); 2439 } 2440 2441 $this->list = []; 2442 $person = Registry::individualFactory()->make($id, $this->tree); 2443 if ($person instanceof Individual) { 2444 $this->list[$id] = $person; 2445 switch ($group) { 2446 case 'child-family': 2447 foreach ($person->childFamilies() as $family) { 2448 foreach ($family->spouses() as $spouse) { 2449 $this->list[$spouse->xref()] = $spouse; 2450 } 2451 2452 foreach ($family->children() as $child) { 2453 $this->list[$child->xref()] = $child; 2454 } 2455 } 2456 break; 2457 case 'spouse-family': 2458 foreach ($person->spouseFamilies() as $family) { 2459 foreach ($family->spouses() as $spouse) { 2460 $this->list[$spouse->xref()] = $spouse; 2461 } 2462 2463 foreach ($family->children() as $child) { 2464 $this->list[$child->xref()] = $child; 2465 } 2466 } 2467 break; 2468 case 'direct-ancestors': 2469 $this->addAncestors($this->list, $id, false, $maxgen); 2470 break; 2471 case 'ancestors': 2472 $this->addAncestors($this->list, $id, true, $maxgen); 2473 break; 2474 case 'descendants': 2475 $this->list[$id]->generation = 1; 2476 $this->addDescendancy($this->list, $id, false, $maxgen); 2477 break; 2478 case 'all': 2479 $this->addAncestors($this->list, $id, true, $maxgen); 2480 $this->addDescendancy($this->list, $id, true, $maxgen); 2481 break; 2482 } 2483 } 2484 2485 switch ($sortby) { 2486 case 'NAME': 2487 uasort($this->list, GedcomRecord::nameComparator()); 2488 break; 2489 case 'BIRT:DATE': 2490 uasort($this->list, Individual::birthDateComparator()); 2491 break; 2492 case 'DEAT:DATE': 2493 uasort($this->list, Individual::deathDateComparator()); 2494 break; 2495 case 'generation': 2496 $newarray = []; 2497 reset($this->list); 2498 $genCounter = 1; 2499 while (count($newarray) < count($this->list)) { 2500 foreach ($this->list as $key => $value) { 2501 $this->generation = $value->generation; 2502 if ($this->generation == $genCounter) { 2503 $newarray[$key] = (object) ['generation' => $this->generation]; 2504 } 2505 } 2506 $genCounter++; 2507 } 2508 $this->list = $newarray; 2509 break; 2510 default: 2511 // unsorted 2512 break; 2513 } 2514 $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes]; 2515 $this->repeat_bytes = xml_get_current_line_number($this->parser) + 1; 2516 } 2517 2518 /** 2519 * Handle </relatives> 2520 * 2521 * @return void 2522 */ 2523 protected function relativesEndHandler(): void 2524 { 2525 $this->process_repeats--; 2526 if ($this->process_repeats > 0) { 2527 return; 2528 } 2529 2530 // Check if there is any relatives 2531 if (count($this->list) > 0) { 2532 $lineoffset = 0; 2533 foreach ($this->repeats_stack as $rep) { 2534 $lineoffset += $rep[1]; 2535 } 2536 //-- read the xml from the file 2537 $lines = file($this->report); 2538 while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<Relatives') && $lineoffset + $this->repeat_bytes > 0) { 2539 $lineoffset--; 2540 } 2541 $lineoffset++; 2542 $reportxml = "<tempdoc>\n"; 2543 $line_nr = $lineoffset + $this->repeat_bytes; 2544 // Relatives Level counter 2545 $count = 1; 2546 while (0 < $count) { 2547 if (str_contains($lines[$line_nr], '<Relatives')) { 2548 $count++; 2549 } elseif (str_contains($lines[$line_nr], '</Relatives')) { 2550 $count--; 2551 } 2552 if (0 < $count) { 2553 $reportxml .= $lines[$line_nr]; 2554 } 2555 $line_nr++; 2556 } 2557 // No need to drag this 2558 unset($lines); 2559 $reportxml .= "</tempdoc>\n"; 2560 // Save original values 2561 $this->parser_stack[] = $this->parser; 2562 $oldgedrec = $this->gedrec; 2563 2564 $this->list_total = count($this->list); 2565 $this->list_private = 0; 2566 foreach ($this->list as $xref => $value) { 2567 if (isset($value->generation)) { 2568 $this->generation = $value->generation; 2569 } 2570 $tmp = Registry::gedcomRecordFactory()->make((string) $xref, $this->tree); 2571 $this->gedrec = $tmp->privatizeGedcom(Auth::accessLevel($this->tree)); 2572 2573 $repeat_parser = xml_parser_create(); 2574 $this->parser = $repeat_parser; 2575 xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false); 2576 2577 xml_set_element_handler( 2578 $repeat_parser, 2579 function ($parser, string $name, array $attrs): void { 2580 $this->startElement($parser, $name, $attrs); 2581 }, 2582 function ($parser, string $name): void { 2583 $this->endElement($parser, $name); 2584 } 2585 ); 2586 2587 xml_set_character_data_handler( 2588 $repeat_parser, 2589 function ($parser, string $data): void { 2590 $this->characterData($parser, $data); 2591 } 2592 ); 2593 2594 if (!xml_parse($repeat_parser, $reportxml, true)) { 2595 throw new DomainException(sprintf('RelativesEHandler XML error: %s at line %d', xml_error_string(xml_get_error_code($repeat_parser)), xml_get_current_line_number($repeat_parser))); 2596 } 2597 xml_parser_free($repeat_parser); 2598 } 2599 // Clean up the list array 2600 $this->list = []; 2601 $this->parser = array_pop($this->parser_stack); 2602 $this->gedrec = $oldgedrec; 2603 } 2604 [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack); 2605 } 2606 2607 /** 2608 * Handle <generation /> 2609 * Prints the number of generations 2610 * 2611 * @return void 2612 */ 2613 protected function generationStartHandler(): void 2614 { 2615 $this->current_element->addText((string) $this->generation); 2616 } 2617 2618 /** 2619 * Handle <newPage /> 2620 * Has to be placed in an element (header, body or footer) 2621 * 2622 * @return void 2623 */ 2624 protected function newPageStartHandler(): void 2625 { 2626 $temp = 'addpage'; 2627 $this->wt_report->addElement($temp); 2628 } 2629 2630 /** 2631 * Handle </title> 2632 * 2633 * @return void 2634 */ 2635 protected function titleEndHandler(): void 2636 { 2637 $this->report_root->addTitle($this->text); 2638 } 2639 2640 /** 2641 * Handle </description> 2642 * 2643 * @return void 2644 */ 2645 protected function descriptionEndHandler(): void 2646 { 2647 $this->report_root->addDescription($this->text); 2648 } 2649 2650 /** 2651 * Create a list of all descendants. 2652 * 2653 * @param array<Individual> $list 2654 * @param string $pid 2655 * @param bool $parents 2656 * @param int $generations 2657 * 2658 * @return void 2659 */ 2660 private function addDescendancy(&$list, $pid, $parents = false, $generations = -1): void 2661 { 2662 $person = Registry::individualFactory()->make($pid, $this->tree); 2663 if ($person === null) { 2664 return; 2665 } 2666 if (!isset($list[$pid])) { 2667 $list[$pid] = $person; 2668 } 2669 if (!isset($list[$pid]->generation)) { 2670 $list[$pid]->generation = 0; 2671 } 2672 foreach ($person->spouseFamilies() as $family) { 2673 if ($parents) { 2674 $husband = $family->husband(); 2675 $wife = $family->wife(); 2676 if ($husband) { 2677 $list[$husband->xref()] = $husband; 2678 if (isset($list[$pid]->generation)) { 2679 $list[$husband->xref()]->generation = $list[$pid]->generation - 1; 2680 } else { 2681 $list[$husband->xref()]->generation = 1; 2682 } 2683 } 2684 if ($wife) { 2685 $list[$wife->xref()] = $wife; 2686 if (isset($list[$pid]->generation)) { 2687 $list[$wife->xref()]->generation = $list[$pid]->generation - 1; 2688 } else { 2689 $list[$wife->xref()]->generation = 1; 2690 } 2691 } 2692 } 2693 2694 $children = $family->children(); 2695 2696 foreach ($children as $child) { 2697 if ($child) { 2698 $list[$child->xref()] = $child; 2699 2700 if (isset($list[$pid]->generation)) { 2701 $list[$child->xref()]->generation = $list[$pid]->generation + 1; 2702 } else { 2703 $list[$child->xref()]->generation = 2; 2704 } 2705 } 2706 } 2707 if ($generations == -1 || $list[$pid]->generation + 1 < $generations) { 2708 foreach ($children as $child) { 2709 $this->addDescendancy($list, $child->xref(), $parents, $generations); // recurse on the childs family 2710 } 2711 } 2712 } 2713 } 2714 2715 /** 2716 * Create a list of all ancestors. 2717 * 2718 * @param array<Individual> $list 2719 * @param string $pid 2720 * @param bool $children 2721 * @param int $generations 2722 * 2723 * @return void 2724 */ 2725 private function addAncestors(array &$list, string $pid, bool $children = false, int $generations = -1): void 2726 { 2727 $genlist = [$pid]; 2728 $list[$pid]->generation = 1; 2729 while (count($genlist) > 0) { 2730 $id = array_shift($genlist); 2731 if (str_starts_with($id, 'empty')) { 2732 continue; // id can be something like “empty7” 2733 } 2734 $person = Registry::individualFactory()->make($id, $this->tree); 2735 foreach ($person->childFamilies() as $family) { 2736 $husband = $family->husband(); 2737 $wife = $family->wife(); 2738 if ($husband) { 2739 $list[$husband->xref()] = $husband; 2740 $list[$husband->xref()]->generation = $list[$id]->generation + 1; 2741 } 2742 if ($wife) { 2743 $list[$wife->xref()] = $wife; 2744 $list[$wife->xref()]->generation = $list[$id]->generation + 1; 2745 } 2746 if ($generations == -1 || $list[$id]->generation + 1 < $generations) { 2747 if ($husband) { 2748 $genlist[] = $husband->xref(); 2749 } 2750 if ($wife) { 2751 $genlist[] = $wife->xref(); 2752 } 2753 } 2754 if ($children) { 2755 foreach ($family->children() as $child) { 2756 $list[$child->xref()] = $child; 2757 $list[$child->xref()]->generation = $list[$id]->generation ?? 1; 2758 } 2759 } 2760 } 2761 } 2762 } 2763 2764 /** 2765 * get gedcom tag value 2766 * 2767 * @param string $tag The tag to find, use : to delineate subtags 2768 * @param int $level The gedcom line level of the first tag to find, setting level to 0 will cause it to use 1+ the level of the incoming record 2769 * @param string $gedrec The gedcom record to get the value from 2770 * 2771 * @return string the value of a gedcom tag from the given gedcom record 2772 */ 2773 private function getGedcomValue(string $tag, int $level, string $gedrec): string 2774 { 2775 if ($gedrec === '') { 2776 return ''; 2777 } 2778 $tags = explode(':', $tag); 2779 $origlevel = $level; 2780 if ($level === 0) { 2781 $level = $gedrec[0] + 1; 2782 } 2783 2784 $subrec = $gedrec; 2785 $t = 'XXXX'; 2786 foreach ($tags as $t) { 2787 $lastsubrec = $subrec; 2788 $subrec = self::getSubRecord($level, "$level $t", $subrec); 2789 if (empty($subrec) && $origlevel == 0) { 2790 $level--; 2791 $subrec = self::getSubRecord($level, "$level $t", $lastsubrec); 2792 } 2793 if (empty($subrec)) { 2794 if ($t === 'TITL') { 2795 $subrec = self::getSubRecord($level, "$level ABBR", $lastsubrec); 2796 if (!empty($subrec)) { 2797 $t = 'ABBR'; 2798 } 2799 } 2800 if ($subrec === '') { 2801 if ($level > 0) { 2802 $level--; 2803 } 2804 $subrec = self::getSubRecord($level, "@ $t", $gedrec); 2805 if ($subrec === '') { 2806 return ''; 2807 } 2808 } 2809 } 2810 $level++; 2811 } 2812 $level--; 2813 $ct = preg_match("/$level $t(.*)/", $subrec, $match); 2814 if ($ct === 0) { 2815 $ct = preg_match("/$level @.+@ (.+)/", $subrec, $match); 2816 } 2817 if ($ct === 0) { 2818 $ct = preg_match("/@ $t (.+)/", $subrec, $match); 2819 } 2820 if ($ct > 0) { 2821 $value = trim($match[1]); 2822 if ($t === 'NOTE' && preg_match('/^@(.+)@$/', $value, $match)) { 2823 $note = Registry::noteFactory()->make($match[1], $this->tree); 2824 if ($note instanceof Note) { 2825 $value = $note->getNote(); 2826 } else { 2827 //-- set the value to the id without the @ 2828 $value = $match[1]; 2829 } 2830 } 2831 if ($level !== 0 || $t !== 'NOTE') { 2832 $value .= self::getCont($level + 1, $subrec); 2833 } 2834 2835 return $value; 2836 } 2837 2838 return ''; 2839 } 2840 2841 /** 2842 * Replace variable identifiers with their values. 2843 * 2844 * @param string $expression An expression such as "$foo == 123" 2845 * @param bool $quote Whether to add quotation marks 2846 * 2847 * @return string 2848 */ 2849 private function substituteVars($expression, $quote): string 2850 { 2851 return preg_replace_callback( 2852 '/\$(\w+)/', 2853 function (array $matches) use ($quote): string { 2854 if (isset($this->vars[$matches[1]]['id'])) { 2855 if ($quote) { 2856 return "'" . addcslashes($this->vars[$matches[1]]['id'], "'") . "'"; 2857 } 2858 2859 return $this->vars[$matches[1]]['id']; 2860 } 2861 2862 Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1])); 2863 2864 return '$' . $matches[1]; 2865 }, 2866 $expression 2867 ); 2868 } 2869} 2870