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