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