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