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