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