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