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