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