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 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_language = new ExpressionLanguage(null, [$expression_provider]); 1468 1469 $value = (string) $expression_language->evaluate($value); 1470 } 1471 1472 if (strpos($value, '@') !== false) { 1473 $value = ''; 1474 } 1475 $this->vars[$name]['id'] = $value; 1476 } 1477 1478 /** 1479 * XML <if > start element 1480 * 1481 * @param array $attrs an array of key value pairs for the attributes 1482 * 1483 * @return void 1484 */ 1485 private function ifStartHandler($attrs) 1486 { 1487 if ($this->process_ifs > 0) { 1488 $this->process_ifs++; 1489 1490 return; 1491 } 1492 1493 $condition = $attrs['condition']; 1494 $condition = $this->substituteVars($condition, true); 1495 $condition = str_replace([ 1496 ' LT ', 1497 ' GT ', 1498 ], [ 1499 '<', 1500 '>', 1501 ], $condition); 1502 // Replace the first accurance only once of @fact:DATE or in any other combinations to the current fact, such as BIRT 1503 $condition = str_replace('@fact:', $this->fact . ':', $condition); 1504 $match = []; 1505 $count = preg_match_all("/@([\w:\.]+)/", $condition, $match, PREG_SET_ORDER); 1506 $i = 0; 1507 while ($i < $count) { 1508 $id = $match[$i][1]; 1509 $value = '""'; 1510 if ($id === 'ID') { 1511 if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) { 1512 $value = "'" . $match[1] . "'"; 1513 } 1514 } elseif ($id === 'fact') { 1515 $value = '"' . $this->fact . '"'; 1516 } elseif ($id === 'desc') { 1517 $value = '"' . addslashes($this->desc) . '"'; 1518 } elseif ($id === 'generation') { 1519 $value = '"' . $this->generation . '"'; 1520 } else { 1521 $temp = explode(' ', trim($this->gedrec)); 1522 $level = $temp[0]; 1523 if ($level == 0) { 1524 $level++; 1525 } 1526 $value = $this->getGedcomValue($id, $level, $this->gedrec); 1527 if (empty($value)) { 1528 $level++; 1529 $value = $this->getGedcomValue($id, $level, $this->gedrec); 1530 } 1531 $value = preg_replace('/^@(' . WT_REGEX_XREF . ')@$/', '$1', $value); 1532 $value = '"' . addslashes($value) . '"'; 1533 } 1534 $condition = str_replace("@$id", $value, $condition); 1535 $i++; 1536 } 1537 1538 // Create an expression language with the functions used by our reports. 1539 $expression_provider = new ReportExpressionLanguageProvider(); 1540 $expression_language = new ExpressionLanguage(null, [$expression_provider]); 1541 1542 $ret = $expression_language->evaluate($condition); 1543 1544 if (!$ret) { 1545 $this->process_ifs++; 1546 } 1547 } 1548 1549 /** 1550 * XML <if /> end element 1551 * 1552 * @return void 1553 */ 1554 private function ifEndHandler() 1555 { 1556 if ($this->process_ifs > 0) { 1557 $this->process_ifs--; 1558 } 1559 } 1560 1561 /** 1562 * XML <Footnote > start element 1563 * Collect the Footnote links 1564 * GEDCOM Records that are protected by Privacy setting will be ignore 1565 * 1566 * @param array $attrs an array of key value pairs for the attributes 1567 * 1568 * @return void 1569 */ 1570 private function footnoteStartHandler($attrs) 1571 { 1572 $id = ''; 1573 if (preg_match('/[0-9] (.+) @(.+)@/', $this->gedrec, $match)) { 1574 $id = $match[2]; 1575 } 1576 $record = GedcomRecord::getInstance($id, $this->tree); 1577 if ($record && $record->canShow()) { 1578 $this->print_data_stack[] = $this->print_data; 1579 $this->print_data = true; 1580 $style = ''; 1581 if (!empty($attrs['style'])) { 1582 $style = $attrs['style']; 1583 } 1584 $this->footnote_element = $this->current_element; 1585 $this->current_element = $this->report_root->createFootnote($style); 1586 } else { 1587 $this->print_data = false; 1588 $this->process_footnote = false; 1589 } 1590 } 1591 1592 /** 1593 * XML <Footnote /> end element 1594 * Print the collected Footnote data 1595 * 1596 * @return void 1597 */ 1598 private function footnoteEndHandler() 1599 { 1600 if ($this->process_footnote) { 1601 $this->print_data = array_pop($this->print_data_stack); 1602 $temp = trim($this->current_element->getValue()); 1603 if (strlen($temp) > 3) { 1604 $this->wt_report->addElement($this->current_element); 1605 } 1606 $this->current_element = $this->footnote_element; 1607 } else { 1608 $this->process_footnote = true; 1609 } 1610 } 1611 1612 /** 1613 * XML <FootnoteTexts /> element 1614 * 1615 * @return void 1616 */ 1617 private function footnoteTextsStartHandler() 1618 { 1619 $temp = 'footnotetexts'; 1620 $this->wt_report->addElement($temp); 1621 } 1622 1623 /** 1624 * XML element Forced line break handler - HTML code 1625 * 1626 * @return void 1627 */ 1628 private function brStartHandler() 1629 { 1630 if ($this->print_data && $this->process_gedcoms === 0) { 1631 $this->current_element->addText('<br>'); 1632 } 1633 } 1634 1635 /** 1636 * XML <sp />element Forced space handler 1637 * 1638 * @return void 1639 */ 1640 private function spStartHandler() 1641 { 1642 if ($this->print_data && $this->process_gedcoms === 0) { 1643 $this->current_element->addText(' '); 1644 } 1645 } 1646 1647 /** 1648 * XML <HighlightedImage/> 1649 * 1650 * @param array $attrs an array of key value pairs for the attributes 1651 * 1652 * @return void 1653 */ 1654 private function highlightedImageStartHandler($attrs) 1655 { 1656 $id = ''; 1657 if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) { 1658 $id = $match[1]; 1659 } 1660 1661 // Position the top corner of this box on the page 1662 $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION); 1663 1664 // Position the left corner of this box on the page 1665 $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION); 1666 1667 // string Align the image in left, center, right (or empty to use x/y position). 1668 $align = $attrs['align'] ?? ''; 1669 1670 // string Next Line should be T:next to the image, N:next line 1671 $ln = $attrs['ln'] ?? 'T'; 1672 1673 // Width, height (or both). 1674 $width = (float) ($attrs['width'] ?? 0.0); 1675 $height = (float) ($attrs['height'] ?? 0.0); 1676 1677 $person = Individual::getInstance($id, $this->tree); 1678 $media_file = $person->findHighlightedMediaFile(); 1679 1680 if ($media_file !== null && $media_file->fileExists()) { 1681 $attributes = getimagesize($media_file->getServerFilename()) ?: [ 1682 0, 1683 0, 1684 ]; 1685 if ($width > 0 && $height == 0) { 1686 $perc = $width / $attributes[0]; 1687 $height = round($attributes[1] * $perc); 1688 } elseif ($height > 0 && $width == 0) { 1689 $perc = $height / $attributes[1]; 1690 $width = round($attributes[0] * $perc); 1691 } else { 1692 $width = $attributes[0]; 1693 $height = $attributes[1]; 1694 } 1695 $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln); 1696 $this->wt_report->addElement($image); 1697 } 1698 } 1699 1700 /** 1701 * XML <Image/> 1702 * 1703 * @param array $attrs an array of key value pairs for the attributes 1704 * 1705 * @return void 1706 */ 1707 private function imageStartHandler($attrs) 1708 { 1709 // Position the top corner of this box on the page. the default is the current position 1710 $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION); 1711 1712 // mixed Position the left corner of this box on the page. the default is the current position 1713 $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION); 1714 1715 // string Align the image in left, center, right (or empty to use x/y position). 1716 $align = $attrs['align'] ?? ''; 1717 1718 // string Next Line should be T:next to the image, N:next line 1719 $ln = $attrs['ln'] ?? 'T'; 1720 1721 // Width, height (or both). 1722 $width = (float) ($attrs['width'] ?? 0.0); 1723 $height = (float) ($attrs['height'] ?? 0.0); 1724 1725 $file = $attrs['file'] ?? ''; 1726 1727 if ($file === '@FILE') { 1728 $match = []; 1729 if (preg_match("/\d OBJE @(.+)@/", $this->gedrec, $match)) { 1730 $mediaobject = Media::getInstance($match[1], $this->tree); 1731 $media_file = $mediaobject->firstImageFile(); 1732 1733 if ($media_file !== null && $media_file->fileExists()) { 1734 $attributes = getimagesize($media_file->getServerFilename()) ?: [ 1735 0, 1736 0, 1737 ]; 1738 if ($width > 0 && $height == 0) { 1739 $perc = $width / $attributes[0]; 1740 $height = round($attributes[1] * $perc); 1741 } elseif ($height > 0 && $width == 0) { 1742 $perc = $height / $attributes[1]; 1743 $width = round($attributes[0] * $perc); 1744 } else { 1745 $width = $attributes[0]; 1746 $height = $attributes[1]; 1747 } 1748 $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln); 1749 $this->wt_report->addElement($image); 1750 } 1751 } 1752 } else { 1753 if (file_exists($file) && preg_match('/(jpg|jpeg|png|gif)$/i', $file)) { 1754 $size = getimagesize($file); 1755 if ($width > 0 && $height == 0) { 1756 $perc = $width / $size[0]; 1757 $height = round($size[1] * $perc); 1758 } elseif ($height > 0 && $width == 0) { 1759 $perc = $height / $size[1]; 1760 $width = round($size[0] * $perc); 1761 } else { 1762 $width = $size[0]; 1763 $height = $size[1]; 1764 } 1765 $image = $this->report_root->createImage($file, $left, $top, $width, $height, $align, $ln); 1766 $this->wt_report->addElement($image); 1767 } 1768 } 1769 } 1770 1771 /** 1772 * XML <Line> element handler 1773 * 1774 * @param array $attrs an array of key value pairs for the attributes 1775 * 1776 * @return void 1777 */ 1778 private function lineStartHandler($attrs) 1779 { 1780 // Start horizontal position, current position (default) 1781 $x1 = ReportBaseElement::CURRENT_POSITION; 1782 if (isset($attrs['x1'])) { 1783 if ($attrs['x1'] === '0') { 1784 $x1 = 0; 1785 } elseif ($attrs['x1'] === '.') { 1786 $x1 = ReportBaseElement::CURRENT_POSITION; 1787 } elseif (!empty($attrs['x1'])) { 1788 $x1 = (float) $attrs['x1']; 1789 } 1790 } 1791 // Start vertical position, current position (default) 1792 $y1 = ReportBaseElement::CURRENT_POSITION; 1793 if (isset($attrs['y1'])) { 1794 if ($attrs['y1'] === '0') { 1795 $y1 = 0; 1796 } elseif ($attrs['y1'] === '.') { 1797 $y1 = ReportBaseElement::CURRENT_POSITION; 1798 } elseif (!empty($attrs['y1'])) { 1799 $y1 = (float) $attrs['y1']; 1800 } 1801 } 1802 // End horizontal position, maximum width (default) 1803 $x2 = ReportBaseElement::CURRENT_POSITION; 1804 if (isset($attrs['x2'])) { 1805 if ($attrs['x2'] === '0') { 1806 $x2 = 0; 1807 } elseif ($attrs['x2'] === '.') { 1808 $x2 = ReportBaseElement::CURRENT_POSITION; 1809 } elseif (!empty($attrs['x2'])) { 1810 $x2 = (float) $attrs['x2']; 1811 } 1812 } 1813 // End vertical position 1814 $y2 = ReportBaseElement::CURRENT_POSITION; 1815 if (isset($attrs['y2'])) { 1816 if ($attrs['y2'] === '0') { 1817 $y2 = 0; 1818 } elseif ($attrs['y2'] === '.') { 1819 $y2 = ReportBaseElement::CURRENT_POSITION; 1820 } elseif (!empty($attrs['y2'])) { 1821 $y2 = (float) $attrs['y2']; 1822 } 1823 } 1824 1825 $line = $this->report_root->createLine($x1, $y1, $x2, $y2); 1826 $this->wt_report->addElement($line); 1827 } 1828 1829 /** 1830 * XML <List> 1831 * 1832 * @param array $attrs an array of key value pairs for the attributes 1833 * 1834 * @return void 1835 */ 1836 private function listStartHandler($attrs) 1837 { 1838 $this->process_repeats++; 1839 if ($this->process_repeats > 1) { 1840 return; 1841 } 1842 1843 $match = []; 1844 if (isset($attrs['sortby'])) { 1845 $sortby = $attrs['sortby']; 1846 if (preg_match("/\\$(\w+)/", $sortby, $match)) { 1847 $sortby = $this->vars[$match[1]]['id']; 1848 $sortby = trim($sortby); 1849 } 1850 } else { 1851 $sortby = 'NAME'; 1852 } 1853 1854 if (isset($attrs['list'])) { 1855 $listname = $attrs['list']; 1856 } else { 1857 $listname = 'individual'; 1858 } 1859 // Some filters/sorts can be applied using SQL, while others require PHP 1860 switch ($listname) { 1861 case 'pending': 1862 $rows = Database::prepare( 1863 "SELECT xref, CASE new_gedcom WHEN '' THEN old_gedcom ELSE new_gedcom END AS gedcom" . 1864 " FROM `##change`" . " WHERE (xref, change_id) IN (" . 1865 " SELECT xref, MAX(change_id)" . 1866 " FROM `##change`" . 1867 " WHERE status = 'pending' AND gedcom_id = :tree_id" . 1868 " GROUP BY xref" . 1869 " )" 1870 )->execute([ 1871 'tree_id' => $this->tree->getTreeId(), 1872 ])->fetchAll(); 1873 $this->list = []; 1874 foreach ($rows as $row) { 1875 $this->list[] = GedcomRecord::getInstance($row->xref, $this->tree, $row->gedcom); 1876 } 1877 break; 1878 case 'individual': 1879 $sql_select = "SELECT i_id AS xref, i_gedcom AS gedcom FROM `##individuals` "; 1880 $sql_join = ""; 1881 $sql_where = " WHERE i_file = :tree_id"; 1882 $sql_order_by = ""; 1883 $sql_params = ['tree_id' => $this->tree->getTreeId()]; 1884 foreach ($attrs as $attr => $value) { 1885 if (strpos($attr, 'filter') === 0 && $value) { 1886 $value = $this->substituteVars($value, false); 1887 // Convert the various filters into SQL 1888 if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) { 1889 $sql_join .= " JOIN `##dates` AS {$attr} ON ({$attr}.d_file=i_file AND {$attr}.d_gid=i_id)"; 1890 $sql_where .= " AND {$attr}.d_fact = :{$attr}fact"; 1891 $sql_params[$attr . 'fact'] = $match[1]; 1892 $date = new Date($match[3]); 1893 if ($match[2] === 'LTE') { 1894 $sql_where .= " AND {$attr}.d_julianday2 <= :{$attr}date"; 1895 $sql_params[$attr . 'date'] = $date->maximumJulianDay(); 1896 } else { 1897 $sql_where .= " AND {$attr}.d_julianday1 >= :{$attr}date"; 1898 $sql_params[$attr . 'date'] = $date->minimumJulianDay(); 1899 } 1900 if ($sortby == $match[1]) { 1901 $sortby = ""; 1902 $sql_order_by .= ($sql_order_by ? ", " : " ORDER BY ") . "{$attr}.d_julianday1"; 1903 } 1904 unset($attrs[$attr]); // This filter has been fully processed 1905 } elseif (preg_match('/^NAME CONTAINS (.*)$/', $value, $match)) { 1906 // Do nothing, unless you have to 1907 if ($match[1] != '' || $sortby === 'NAME') { 1908 $sql_join .= " JOIN `##name` AS {$attr} ON (n_file=i_file AND n_id=i_id)"; 1909 // Search the DB only if there is any name supplied 1910 if ($match[1] != '') { 1911 $names = explode(' ', $match[1]); 1912 foreach ($names as $n => $name) { 1913 $sql_where .= " AND {$attr}.n_full LIKE CONCAT('%', :{$attr}name{$n}, '%')"; 1914 $sql_params[$attr . 'name' . $n] = $name; 1915 } 1916 } 1917 // Let the DB do the name sorting even when no name was entered 1918 if ($sortby === 'NAME') { 1919 $sortby = ''; 1920 $sql_order_by .= ($sql_order_by ? ', ' : ' ORDER BY ') . "{$attr}.n_sort"; 1921 } 1922 } 1923 unset($attrs[$attr]); // This filter has been fully processed 1924 } elseif (preg_match('/^REGEXP \/(.+)\//', $value, $match)) { 1925 $sql_where .= " AND i_gedcom REGEXP :{$attr}gedcom"; 1926 // PDO helpfully escapes backslashes for us, preventing us from matching "\n1 FACT" 1927 $sql_params[$attr . 'gedcom'] = str_replace('\n', "\n", $match[1]); 1928 unset($attrs[$attr]); // This filter has been fully processed 1929 } elseif (preg_match('/^(?:\w+):PLAC CONTAINS (.+)$/', $value, $match)) { 1930 // Don't unset this filter. This is just initial filtering 1931 $sql_join .= " JOIN `##places` AS {$attr}a ON ({$attr}a.p_file = i_file)"; 1932 $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)"; 1933 $sql_where .= " AND {$attr}a.p_place LIKE CONCAT('%', :{$attr}place, '%')"; 1934 $sql_params[$attr . 'place'] = $match[1]; 1935 } elseif (preg_match('/^(\w*):*(\w*) CONTAINS (.+)$/', $value, $match)) { 1936 // Don't unset this filter. This is just initial filtering 1937 $sql_where .= " AND i_gedcom LIKE CONCAT('%', :{$attr}contains1, '%', :{$attr}contains2, '%', :{$attr}contains3, '%')"; 1938 $sql_params[$attr . 'contains1'] = $match[1]; 1939 $sql_params[$attr . 'contains2'] = $match[2]; 1940 $sql_params[$attr . 'contains3'] = $match[3]; 1941 } 1942 } 1943 } 1944 1945 $this->list = []; 1946 $rows = Database::prepare( 1947 $sql_select . $sql_join . $sql_where . $sql_order_by 1948 )->execute($sql_params)->fetchAll(); 1949 1950 foreach ($rows as $row) { 1951 $this->list[$row->xref] = Individual::getInstance($row->xref, $this->tree, $row->gedcom); 1952 } 1953 break; 1954 1955 case 'family': 1956 $sql_select = "SELECT f_id AS xref, f_gedcom AS gedcom FROM `##families`"; 1957 $sql_join = ""; 1958 $sql_where = " WHERE f_file = :tree_id"; 1959 $sql_order_by = ""; 1960 $sql_params = ['tree_id' => $this->tree->getTreeId()]; 1961 foreach ($attrs as $attr => $value) { 1962 if (strpos($attr, 'filter') === 0 && $value) { 1963 $value = $this->substituteVars($value, false); 1964 // Convert the various filters into SQL 1965 if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) { 1966 $sql_join .= " JOIN `##dates` AS {$attr} ON ({$attr}.d_file=f_file AND {$attr}.d_gid=f_id)"; 1967 $sql_where .= " AND {$attr}.d_fact = :{$attr}fact"; 1968 $sql_params[$attr . 'fact'] = $match[1]; 1969 $date = new Date($match[3]); 1970 if ($match[2] === 'LTE') { 1971 $sql_where .= " AND {$attr}.d_julianday2 <= :{$attr}date"; 1972 $sql_params[$attr . 'date'] = $date->maximumJulianDay(); 1973 } else { 1974 $sql_where .= " AND {$attr}.d_julianday1 >= :{$attr}date"; 1975 $sql_params[$attr . 'date'] = $date->minimumJulianDay(); 1976 } 1977 if ($sortby == $match[1]) { 1978 $sortby = ''; 1979 $sql_order_by .= ($sql_order_by ? ', ' : ' ORDER BY ') . "{$attr}.d_julianday1"; 1980 } 1981 unset($attrs[$attr]); // This filter has been fully processed 1982 } elseif (preg_match('/^REGEXP \/(.+)\//', $value, $match)) { 1983 $sql_where .= " AND f_gedcom REGEXP :{$attr}gedcom"; 1984 // PDO helpfully escapes backslashes for us, preventing us from matching "\n1 FACT" 1985 $sql_params[$attr . 'gedcom'] = str_replace('\n', "\n", $match[1]); 1986 unset($attrs[$attr]); // This filter has been fully processed 1987 } elseif (preg_match('/^NAME CONTAINS (.+)$/', $value, $match)) { 1988 // Do nothing, unless you have to 1989 if ($match[1] != '' || $sortby === 'NAME') { 1990 $sql_join .= " JOIN `##name` AS {$attr} ON n_file = f_file AND n_id IN (f_husb, f_wife)"; 1991 // Search the DB only if there is any name supplied 1992 if ($match[1] != '') { 1993 $names = explode(' ', $match[1]); 1994 foreach ($names as $n => $name) { 1995 $sql_where .= " AND {$attr}.n_full LIKE CONCAT('%', :{$attr}name{$n}, '%')"; 1996 $sql_params[$attr . 'name' . $n] = $name; 1997 } 1998 } 1999 // Let the DB do the name sorting even when no name was entered 2000 if ($sortby === 'NAME') { 2001 $sortby = ''; 2002 $sql_order_by .= ($sql_order_by ? ', ' : ' ORDER BY ') . "{$attr}.n_sort"; 2003 } 2004 } 2005 unset($attrs[$attr]); // This filter has been fully processed 2006 } elseif (preg_match('/^(?:\w+):PLAC CONTAINS (.+)$/', $value, $match)) { 2007 $sql_join .= " JOIN `##places` AS {$attr}a ON ({$attr}a.p_file=f_file)"; 2008 $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)"; 2009 $sql_where .= " AND {$attr}a.p_place LIKE CONCAT('%', :{$attr}place, '%')"; 2010 $sql_params[$attr . 'place'] = $match[1]; 2011 // Don't unset this filter. This is just initial filtering 2012 } elseif (preg_match('/^(\w*):*(\w*) CONTAINS (.+)$/', $value, $match)) { 2013 $sql_where .= " AND f_gedcom LIKE CONCAT('%', :{$attr}contains1, '%', :{$attr}contains2, '%', :{$attr}contains3, '%')"; 2014 $sql_params[$attr . 'contains1'] = $match[1]; 2015 $sql_params[$attr . 'contains2'] = $match[2]; 2016 $sql_params[$attr . 'contains3'] = $match[3]; 2017 // Don't unset this filter. This is just initial filtering 2018 } 2019 } 2020 } 2021 2022 $this->list = []; 2023 $rows = Database::prepare( 2024 $sql_select . $sql_join . $sql_where . $sql_order_by 2025 )->execute($sql_params)->fetchAll(); 2026 2027 foreach ($rows as $row) { 2028 $this->list[$row->xref] = Family::getInstance($row->xref, $this->tree, $row->gedcom); 2029 } 2030 break; 2031 2032 default: 2033 throw new \DomainException('Invalid list name: ' . $listname); 2034 } 2035 2036 $filters = []; 2037 $filters2 = []; 2038 if (isset($attrs['filter1']) && count($this->list) > 0) { 2039 foreach ($attrs as $key => $value) { 2040 if (preg_match("/filter(\d)/", $key)) { 2041 $condition = $value; 2042 if (preg_match("/@(\w+)/", $condition, $match)) { 2043 $id = $match[1]; 2044 $value = "''"; 2045 if ($id === 'ID') { 2046 if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) { 2047 $value = "'" . $match[1] . "'"; 2048 } 2049 } elseif ($id === 'fact') { 2050 $value = "'" . $this->fact . "'"; 2051 } elseif ($id === 'desc') { 2052 $value = "'" . $this->desc . "'"; 2053 } else { 2054 if (preg_match("/\d $id (.+)/", $this->gedrec, $match)) { 2055 $value = "'" . str_replace('@', '', trim($match[1])) . "'"; 2056 } 2057 } 2058 $condition = preg_replace("/@$id/", $value, $condition); 2059 } 2060 //-- handle regular expressions 2061 if (preg_match("/([A-Z:]+)\s*([^\s]+)\s*(.+)/", $condition, $match)) { 2062 $tag = trim($match[1]); 2063 $expr = trim($match[2]); 2064 $val = trim($match[3]); 2065 if (preg_match("/\\$(\w+)/", $val, $match)) { 2066 $val = $this->vars[$match[1]]['id']; 2067 $val = trim($val); 2068 } 2069 if ($val) { 2070 $searchstr = ''; 2071 $tags = explode(':', $tag); 2072 //-- only limit to a level number if we are specifically looking at a level 2073 if (count($tags) > 1) { 2074 $level = 1; 2075 foreach ($tags as $t) { 2076 if (!empty($searchstr)) { 2077 $searchstr .= "[^\n]*(\n[2-9][^\n]*)*\n"; 2078 } 2079 //-- search for both EMAIL and _EMAIL... silly double gedcom standard 2080 if ($t === 'EMAIL' || $t === '_EMAIL') { 2081 $t = '_?EMAIL'; 2082 } 2083 $searchstr .= $level . ' ' . $t; 2084 $level++; 2085 } 2086 } else { 2087 if ($tag === 'EMAIL' || $tag === '_EMAIL') { 2088 $tag = '_?EMAIL'; 2089 } 2090 $t = $tag; 2091 $searchstr = '1 ' . $tag; 2092 } 2093 switch ($expr) { 2094 case 'CONTAINS': 2095 if ($t === 'PLAC') { 2096 $searchstr .= "[^\n]*[, ]*" . $val; 2097 } else { 2098 $searchstr .= "[^\n]*" . $val; 2099 } 2100 $filters[] = $searchstr; 2101 break; 2102 default: 2103 $filters2[] = [ 2104 'tag' => $tag, 2105 'expr' => $expr, 2106 'val' => $val, 2107 ]; 2108 break; 2109 } 2110 } 2111 } 2112 } 2113 } 2114 } 2115 //-- apply other filters to the list that could not be added to the search string 2116 if ($filters) { 2117 foreach ($this->list as $key => $record) { 2118 foreach ($filters as $filter) { 2119 if (!preg_match('/' . $filter . '/i', $record->privatizeGedcom(Auth::accessLevel($this->tree)))) { 2120 unset($this->list[$key]); 2121 break; 2122 } 2123 } 2124 } 2125 } 2126 if ($filters2) { 2127 $mylist = []; 2128 foreach ($this->list as $indi) { 2129 $key = $indi->getXref(); 2130 $grec = $indi->privatizeGedcom(Auth::accessLevel($this->tree)); 2131 $keep = true; 2132 foreach ($filters2 as $filter) { 2133 if ($keep) { 2134 $tag = $filter['tag']; 2135 $expr = $filter['expr']; 2136 $val = $filter['val']; 2137 if ($val == "''") { 2138 $val = ''; 2139 } 2140 $tags = explode(':', $tag); 2141 $t = end($tags); 2142 $v = $this->getGedcomValue($tag, 1, $grec); 2143 //-- check for EMAIL and _EMAIL (silly double gedcom standard :P) 2144 if ($t === 'EMAIL' && empty($v)) { 2145 $tag = str_replace('EMAIL', '_EMAIL', $tag); 2146 $tags = explode(':', $tag); 2147 $t = end($tags); 2148 $v = Functions::getSubRecord(1, $tag, $grec); 2149 } 2150 2151 switch ($expr) { 2152 case 'GTE': 2153 if ($t === 'DATE') { 2154 $date1 = new Date($v); 2155 $date2 = new Date($val); 2156 $keep = (Date::compare($date1, $date2) >= 0); 2157 } elseif ($val >= $v) { 2158 $keep = true; 2159 } 2160 break; 2161 case 'LTE': 2162 if ($t === 'DATE') { 2163 $date1 = new Date($v); 2164 $date2 = new Date($val); 2165 $keep = (Date::compare($date1, $date2) <= 0); 2166 } elseif ($val >= $v) { 2167 $keep = true; 2168 } 2169 break; 2170 default: 2171 if ($v == $val) { 2172 $keep = true; 2173 } else { 2174 $keep = false; 2175 } 2176 break; 2177 } 2178 } 2179 } 2180 if ($keep) { 2181 $mylist[$key] = $indi; 2182 } 2183 } 2184 $this->list = $mylist; 2185 } 2186 2187 switch ($sortby) { 2188 case 'NAME': 2189 uasort($this->list, '\Fisharebest\Webtrees\GedcomRecord::compare'); 2190 break; 2191 case 'CHAN': 2192 uasort($this->list, function (GedcomRecord $x, GedcomRecord $y): int { 2193 return $y->lastChangeTimestamp(true) - $x->lastChangeTimestamp(true); 2194 }); 2195 break; 2196 case 'BIRT:DATE': 2197 uasort($this->list, '\Fisharebest\Webtrees\Individual::compareBirthDate'); 2198 break; 2199 case 'DEAT:DATE': 2200 uasort($this->list, '\Fisharebest\Webtrees\Individual::compareDeathDate'); 2201 break; 2202 case 'MARR:DATE': 2203 uasort($this->list, '\Fisharebest\Webtrees\Family::compareMarrDate'); 2204 break; 2205 default: 2206 // unsorted or already sorted by SQL 2207 break; 2208 } 2209 2210 $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes]; 2211 $this->repeat_bytes = xml_get_current_line_number($this->parser) + 1; 2212 } 2213 2214 /** 2215 * XML <List> 2216 * 2217 * @return void 2218 */ 2219 private function listEndHandler() 2220 { 2221 $this->process_repeats--; 2222 if ($this->process_repeats > 0) { 2223 return; 2224 } 2225 2226 // Check if there is any list 2227 if (count($this->list) > 0) { 2228 $lineoffset = 0; 2229 foreach ($this->repeats_stack as $rep) { 2230 $lineoffset += $rep[1]; 2231 } 2232 //-- read the xml from the file 2233 $lines = file($this->report); 2234 while ((strpos($lines[$lineoffset + $this->repeat_bytes], '<List') === false) && (($lineoffset + $this->repeat_bytes) > 0)) { 2235 $lineoffset--; 2236 } 2237 $lineoffset++; 2238 $reportxml = "<tempdoc>\n"; 2239 $line_nr = $lineoffset + $this->repeat_bytes; 2240 // List Level counter 2241 $count = 1; 2242 while (0 < $count) { 2243 if (strpos($lines[$line_nr], '<List') !== false) { 2244 $count++; 2245 } elseif (strpos($lines[$line_nr], '</List') !== false) { 2246 $count--; 2247 } 2248 if (0 < $count) { 2249 $reportxml .= $lines[$line_nr]; 2250 } 2251 $line_nr++; 2252 } 2253 // No need to drag this 2254 unset($lines); 2255 $reportxml .= '</tempdoc>'; 2256 // Save original values 2257 $this->parser_stack[] = $this->parser; 2258 $oldgedrec = $this->gedrec; 2259 2260 $this->list_total = count($this->list); 2261 $this->list_private = 0; 2262 foreach ($this->list as $record) { 2263 if ($record->canShow()) { 2264 $this->gedrec = $record->privatizeGedcom(Auth::accessLevel($record->getTree())); 2265 //-- start the sax parser 2266 $repeat_parser = xml_parser_create(); 2267 $this->parser = $repeat_parser; 2268 xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false); 2269 2270 xml_set_element_handler( 2271 $repeat_parser, 2272 function ($parser, string $name, array $attrs) { 2273 $this->startElement($parser, $name, $attrs); 2274 }, 2275 function ($parser, string $name) { 2276 $this->endElement($parser, $name); 2277 } 2278 ); 2279 2280 xml_set_character_data_handler( 2281 $repeat_parser, 2282 function ($parser, $data) { 2283 $this->characterData($parser, $data); 2284 } 2285 ); 2286 2287 if (!xml_parse($repeat_parser, $reportxml, true)) { 2288 throw new \DomainException(sprintf( 2289 'ListEHandler XML error: %s at line %d', 2290 xml_error_string(xml_get_error_code($repeat_parser)), 2291 xml_get_current_line_number($repeat_parser) 2292 )); 2293 } 2294 xml_parser_free($repeat_parser); 2295 } else { 2296 $this->list_private++; 2297 } 2298 } 2299 $this->list = []; 2300 $this->parser = array_pop($this->parser_stack); 2301 $this->gedrec = $oldgedrec; 2302 } 2303 list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack); 2304 } 2305 2306 /** 2307 * XML <ListTotal> element handler 2308 * Prints the total number of records in a list 2309 * The total number is collected from 2310 * List and Relatives 2311 * 2312 * @return void 2313 */ 2314 private function listTotalStartHandler() 2315 { 2316 if ($this->list_private == 0) { 2317 $this->current_element->addText((string) $this->list_total); 2318 } else { 2319 $this->current_element->addText(($this->list_total - $this->list_private) . ' / ' . $this->list_total); 2320 } 2321 } 2322 2323 /** 2324 * XML <Relatives> 2325 * 2326 * @param array $attrs an array of key value pairs for the attributes 2327 * 2328 * @return void 2329 */ 2330 private function relativesStartHandler($attrs) 2331 { 2332 $this->process_repeats++; 2333 if ($this->process_repeats > 1) { 2334 return; 2335 } 2336 2337 $sortby = 'NAME'; 2338 if (isset($attrs['sortby'])) { 2339 $sortby = $attrs['sortby']; 2340 } 2341 $match = []; 2342 if (preg_match("/\\$(\w+)/", $sortby, $match)) { 2343 $sortby = $this->vars[$match[1]]['id']; 2344 $sortby = trim($sortby); 2345 } 2346 2347 $maxgen = -1; 2348 if (isset($attrs['maxgen'])) { 2349 $maxgen = $attrs['maxgen']; 2350 } 2351 if ($maxgen === '*') { 2352 $maxgen = -1; 2353 } 2354 2355 $group = 'child-family'; 2356 if (isset($attrs['group'])) { 2357 $group = $attrs['group']; 2358 } 2359 if (preg_match("/\\$(\w+)/", $group, $match)) { 2360 $group = $this->vars[$match[1]]['id']; 2361 $group = trim($group); 2362 } 2363 2364 $id = ''; 2365 if (isset($attrs['id'])) { 2366 $id = $attrs['id']; 2367 } 2368 if (preg_match("/\\$(\w+)/", $id, $match)) { 2369 $id = $this->vars[$match[1]]['id']; 2370 $id = trim($id); 2371 } 2372 2373 $this->list = []; 2374 $person = Individual::getInstance($id, $this->tree); 2375 if (!empty($person)) { 2376 $this->list[$id] = $person; 2377 switch ($group) { 2378 case 'child-family': 2379 foreach ($person->getChildFamilies() as $family) { 2380 $husband = $family->getHusband(); 2381 $wife = $family->getWife(); 2382 if (!empty($husband)) { 2383 $this->list[$husband->getXref()] = $husband; 2384 } 2385 if (!empty($wife)) { 2386 $this->list[$wife->getXref()] = $wife; 2387 } 2388 $children = $family->getChildren(); 2389 foreach ($children as $child) { 2390 if (!empty($child)) { 2391 $this->list[$child->getXref()] = $child; 2392 } 2393 } 2394 } 2395 break; 2396 case 'spouse-family': 2397 foreach ($person->getSpouseFamilies() as $family) { 2398 $husband = $family->getHusband(); 2399 $wife = $family->getWife(); 2400 if (!empty($husband)) { 2401 $this->list[$husband->getXref()] = $husband; 2402 } 2403 if (!empty($wife)) { 2404 $this->list[$wife->getXref()] = $wife; 2405 } 2406 $children = $family->getChildren(); 2407 foreach ($children as $child) { 2408 if (!empty($child)) { 2409 $this->list[$child->getXref()] = $child; 2410 } 2411 } 2412 } 2413 break; 2414 case 'direct-ancestors': 2415 $this->addAncestors($this->list, $id, false, $maxgen); 2416 break; 2417 case 'ancestors': 2418 $this->addAncestors($this->list, $id, true, $maxgen); 2419 break; 2420 case 'descendants': 2421 $this->list[$id]->generation = 1; 2422 $this->addDescendancy($this->list, $id, false, $maxgen); 2423 break; 2424 case 'all': 2425 $this->addAncestors($this->list, $id, true, $maxgen); 2426 $this->addDescendancy($this->list, $id, true, $maxgen); 2427 break; 2428 } 2429 } 2430 2431 switch ($sortby) { 2432 case 'NAME': 2433 uasort($this->list, '\Fisharebest\Webtrees\GedcomRecord::compare'); 2434 break; 2435 case 'BIRT:DATE': 2436 uasort($this->list, '\Fisharebest\Webtrees\Individual::compareBirthDate'); 2437 break; 2438 case 'DEAT:DATE': 2439 uasort($this->list, '\Fisharebest\Webtrees\Individual::compareDeathDate'); 2440 break; 2441 case 'generation': 2442 $newarray = []; 2443 reset($this->list); 2444 $genCounter = 1; 2445 while (count($newarray) < count($this->list)) { 2446 foreach ($this->list as $key => $value) { 2447 $this->generation = $value->generation; 2448 if ($this->generation == $genCounter) { 2449 $newarray[$key] = new stdClass(); 2450 $newarray[$key]->generation = $this->generation; 2451 } 2452 } 2453 $genCounter++; 2454 } 2455 $this->list = $newarray; 2456 break; 2457 default: 2458 // unsorted 2459 break; 2460 } 2461 $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes]; 2462 $this->repeat_bytes = xml_get_current_line_number($this->parser) + 1; 2463 } 2464 2465 /** 2466 * XML </ Relatives> 2467 * 2468 * @return void 2469 */ 2470 private function relativesEndHandler() 2471 { 2472 $this->process_repeats--; 2473 if ($this->process_repeats > 0) { 2474 return; 2475 } 2476 2477 // Check if there is any relatives 2478 if (count($this->list) > 0) { 2479 $lineoffset = 0; 2480 foreach ($this->repeats_stack as $rep) { 2481 $lineoffset += $rep[1]; 2482 } 2483 //-- read the xml from the file 2484 $lines = file($this->report); 2485 while ((strpos($lines[$lineoffset + $this->repeat_bytes], '<Relatives') === false) && (($lineoffset + $this->repeat_bytes) > 0)) { 2486 $lineoffset--; 2487 } 2488 $lineoffset++; 2489 $reportxml = "<tempdoc>\n"; 2490 $line_nr = $lineoffset + $this->repeat_bytes; 2491 // Relatives Level counter 2492 $count = 1; 2493 while (0 < $count) { 2494 if (strpos($lines[$line_nr], '<Relatives') !== false) { 2495 $count++; 2496 } elseif (strpos($lines[$line_nr], '</Relatives') !== false) { 2497 $count--; 2498 } 2499 if (0 < $count) { 2500 $reportxml .= $lines[$line_nr]; 2501 } 2502 $line_nr++; 2503 } 2504 // No need to drag this 2505 unset($lines); 2506 $reportxml .= "</tempdoc>\n"; 2507 // Save original values 2508 $this->parser_stack[] = $this->parser; 2509 $oldgedrec = $this->gedrec; 2510 2511 $this->list_total = count($this->list); 2512 $this->list_private = 0; 2513 foreach ($this->list as $key => $value) { 2514 if (isset($value->generation)) { 2515 $this->generation = $value->generation; 2516 } 2517 $tmp = GedcomRecord::getInstance($key, $this->tree); 2518 $this->gedrec = $tmp->privatizeGedcom(Auth::accessLevel($this->tree)); 2519 2520 $repeat_parser = xml_parser_create(); 2521 $this->parser = $repeat_parser; 2522 xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false); 2523 2524 xml_set_element_handler( 2525 $repeat_parser, 2526 function ($parser, string $name, array $attrs) { 2527 $this->startElement($parser, $name, $attrs); 2528 }, 2529 function ($parser, string $name) { 2530 $this->endElement($parser, $name); 2531 } 2532 ); 2533 2534 xml_set_character_data_handler( 2535 $repeat_parser, 2536 function ($parser, $data) { 2537 $this->characterData($parser, $data); 2538 } 2539 ); 2540 2541 if (!xml_parse($repeat_parser, $reportxml, true)) { 2542 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))); 2543 } 2544 xml_parser_free($repeat_parser); 2545 } 2546 // Clean up the list array 2547 $this->list = []; 2548 $this->parser = array_pop($this->parser_stack); 2549 $this->gedrec = $oldgedrec; 2550 } 2551 list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack); 2552 } 2553 2554 /** 2555 * XML <Generation /> element handler 2556 * Prints the number of generations 2557 * 2558 * @return void 2559 */ 2560 private function generationStartHandler() 2561 { 2562 $this->current_element->addText((string) $this->generation); 2563 } 2564 2565 /** 2566 * XML <NewPage /> element handler 2567 * Has to be placed in an element (header, pageheader, body or footer) 2568 * 2569 * @return void 2570 */ 2571 private function newPageStartHandler() 2572 { 2573 $temp = 'addpage'; 2574 $this->wt_report->addElement($temp); 2575 } 2576 2577 /** 2578 * XML <html> 2579 * 2580 * @param string $tag HTML tag name 2581 * @param array[] $attrs an array of key value pairs for the attributes 2582 * 2583 * @return void 2584 */ 2585 private function htmlStartHandler($tag, $attrs) 2586 { 2587 if ($tag === 'tempdoc') { 2588 return; 2589 } 2590 $this->wt_report_stack[] = $this->wt_report; 2591 $this->wt_report = $this->report_root->createHTML($tag, $attrs); 2592 $this->current_element = $this->wt_report; 2593 2594 $this->print_data_stack[] = $this->print_data; 2595 $this->print_data = true; 2596 } 2597 2598 /** 2599 * XML </html> 2600 * 2601 * @param string $tag 2602 * 2603 * @return void 2604 */ 2605 private function htmlEndHandler($tag) 2606 { 2607 if ($tag === 'tempdoc') { 2608 return; 2609 } 2610 2611 $this->print_data = array_pop($this->print_data_stack); 2612 $this->current_element = $this->wt_report; 2613 $this->wt_report = array_pop($this->wt_report_stack); 2614 if ($this->wt_report !== null) { 2615 $this->wt_report->addElement($this->current_element); 2616 } else { 2617 $this->wt_report = $this->current_element; 2618 } 2619 } 2620 2621 /** 2622 * Handle <Input> 2623 * 2624 * @return void 2625 */ 2626 private function inputStartHandler() 2627 { 2628 // Dummy function, to prevent the default HtmlStartHandler() being called 2629 } 2630 2631 /** 2632 * Handle </Input> 2633 * 2634 * @return void 2635 */ 2636 private function inputEndHandler() 2637 { 2638 // Dummy function, to prevent the default HtmlEndHandler() being called 2639 } 2640 2641 /** 2642 * Handle <Report> 2643 * 2644 * @return void 2645 */ 2646 private function reportStartHandler() 2647 { 2648 // Dummy function, to prevent the default HtmlStartHandler() being called 2649 } 2650 2651 /** 2652 * Handle </Report> 2653 * 2654 * @return void 2655 */ 2656 private function reportEndHandler() 2657 { 2658 // Dummy function, to prevent the default HtmlEndHandler() being called 2659 } 2660 2661 /** 2662 * XML </titleEndHandler> 2663 * 2664 * @return void 2665 */ 2666 private function titleEndHandler() 2667 { 2668 $this->report_root->addTitle($this->text); 2669 } 2670 2671 /** 2672 * XML </descriptionEndHandler> 2673 * 2674 * @return void 2675 */ 2676 private function descriptionEndHandler() 2677 { 2678 $this->report_root->addDescription($this->text); 2679 } 2680 2681 /** 2682 * Create a list of all descendants. 2683 * 2684 * @param string[] $list 2685 * @param string $pid 2686 * @param bool $parents 2687 * @param int $generations 2688 * 2689 * @return void 2690 */ 2691 private function addDescendancy(&$list, $pid, $parents = false, $generations = -1) 2692 { 2693 $person = Individual::getInstance($pid, $this->tree); 2694 if ($person === null) { 2695 return; 2696 } 2697 if (!isset($list[$pid])) { 2698 $list[$pid] = $person; 2699 } 2700 if (!isset($list[$pid]->generation)) { 2701 $list[$pid]->generation = 0; 2702 } 2703 foreach ($person->getSpouseFamilies() as $family) { 2704 if ($parents) { 2705 $husband = $family->getHusband(); 2706 $wife = $family->getWife(); 2707 if ($husband) { 2708 $list[$husband->getXref()] = $husband; 2709 if (isset($list[$pid]->generation)) { 2710 $list[$husband->getXref()]->generation = $list[$pid]->generation - 1; 2711 } else { 2712 $list[$husband->getXref()]->generation = 1; 2713 } 2714 } 2715 if ($wife) { 2716 $list[$wife->getXref()] = $wife; 2717 if (isset($list[$pid]->generation)) { 2718 $list[$wife->getXref()]->generation = $list[$pid]->generation - 1; 2719 } else { 2720 $list[$wife->getXref()]->generation = 1; 2721 } 2722 } 2723 } 2724 $children = $family->getChildren(); 2725 foreach ($children as $child) { 2726 if ($child) { 2727 $list[$child->getXref()] = $child; 2728 if (isset($list[$pid]->generation)) { 2729 $list[$child->getXref()]->generation = $list[$pid]->generation + 1; 2730 } else { 2731 $list[$child->getXref()]->generation = 2; 2732 } 2733 } 2734 } 2735 if ($generations == -1 || $list[$pid]->generation + 1 < $generations) { 2736 foreach ($children as $child) { 2737 $this->addDescendancy($list, $child->getXref(), $parents, $generations); // recurse on the childs family 2738 } 2739 } 2740 } 2741 } 2742 2743 /** 2744 * Create a list of all ancestors. 2745 * 2746 * @param string[] $list 2747 * @param string $pid 2748 * @param bool $children 2749 * @param int $generations 2750 * 2751 * @return void 2752 */ 2753 private function addAncestors(&$list, $pid, $children = false, $generations = -1) 2754 { 2755 $genlist = [$pid]; 2756 $list[$pid]->generation = 1; 2757 while (count($genlist) > 0) { 2758 $id = array_shift($genlist); 2759 if (strpos($id, 'empty') === 0) { 2760 continue; // id can be something like “empty7” 2761 } 2762 $person = Individual::getInstance($id, $this->tree); 2763 foreach ($person->getChildFamilies() as $family) { 2764 $husband = $family->getHusband(); 2765 $wife = $family->getWife(); 2766 if ($husband) { 2767 $list[$husband->getXref()] = $husband; 2768 $list[$husband->getXref()]->generation = $list[$id]->generation + 1; 2769 } 2770 if ($wife) { 2771 $list[$wife->getXref()] = $wife; 2772 $list[$wife->getXref()]->generation = $list[$id]->generation + 1; 2773 } 2774 if ($generations == -1 || $list[$id]->generation + 1 < $generations) { 2775 if ($husband) { 2776 $genlist[] = $husband->getXref(); 2777 } 2778 if ($wife) { 2779 $genlist[] = $wife->getXref(); 2780 } 2781 } 2782 if ($children) { 2783 foreach ($family->getChildren() as $child) { 2784 $list[$child->getXref()] = $child; 2785 if (isset($list[$id]->generation)) { 2786 $list[$child->getXref()]->generation = $list[$id]->generation; 2787 } else { 2788 $list[$child->getXref()]->generation = 1; 2789 } 2790 } 2791 } 2792 } 2793 } 2794 } 2795 2796 /** 2797 * get gedcom tag value 2798 * 2799 * @param string $tag The tag to find, use : to delineate subtags 2800 * @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 2801 * @param string $gedrec The gedcom record to get the value from 2802 * 2803 * @return string the value of a gedcom tag from the given gedcom record 2804 */ 2805 private function getGedcomValue($tag, $level, $gedrec): string 2806 { 2807 if (empty($gedrec)) { 2808 return ''; 2809 } 2810 $tags = explode(':', $tag); 2811 $origlevel = $level; 2812 if ($level == 0) { 2813 $level = $gedrec[0] + 1; 2814 } 2815 2816 $subrec = $gedrec; 2817 foreach ($tags as $t) { 2818 $lastsubrec = $subrec; 2819 $subrec = Functions::getSubRecord($level, "$level $t", $subrec); 2820 if (empty($subrec) && $origlevel == 0) { 2821 $level--; 2822 $subrec = Functions::getSubRecord($level, "$level $t", $lastsubrec); 2823 } 2824 if (empty($subrec)) { 2825 if ($t === 'TITL') { 2826 $subrec = Functions::getSubRecord($level, "$level ABBR", $lastsubrec); 2827 if (!empty($subrec)) { 2828 $t = 'ABBR'; 2829 } 2830 } 2831 if (empty($subrec)) { 2832 if ($level > 0) { 2833 $level--; 2834 } 2835 $subrec = Functions::getSubRecord($level, "@ $t", $gedrec); 2836 if (empty($subrec)) { 2837 return ''; 2838 } 2839 } 2840 } 2841 $level++; 2842 } 2843 $level--; 2844 $ct = preg_match("/$level $t(.*)/", $subrec, $match); 2845 if ($ct == 0) { 2846 $ct = preg_match("/$level @.+@ (.+)/", $subrec, $match); 2847 } 2848 if ($ct == 0) { 2849 $ct = preg_match("/@ $t (.+)/", $subrec, $match); 2850 } 2851 if ($ct > 0) { 2852 $value = trim($match[1]); 2853 if ($t === 'NOTE' && preg_match('/^@(.+)@$/', $value, $match)) { 2854 $note = Note::getInstance($match[1], $this->tree); 2855 if ($note instanceof Note) { 2856 $value = $note->getNote(); 2857 } else { 2858 //-- set the value to the id without the @ 2859 $value = $match[1]; 2860 } 2861 } 2862 if ($level != 0 || $t != 'NOTE') { 2863 $value .= Functions::getCont($level + 1, $subrec); 2864 } 2865 2866 return $value; 2867 } 2868 2869 return ''; 2870 } 2871 2872 /** 2873 * Replace variable identifiers with their values. 2874 * 2875 * @param string $expression An expression such as "$foo == 123" 2876 * @param bool $quote Whether to add quotation marks 2877 * 2878 * @return string 2879 */ 2880 private function substituteVars($expression, $quote): string 2881 { 2882 return preg_replace_callback( 2883 '/\$(\w+)/', 2884 function (array $matches) use ($quote): string { 2885 if (isset($this->vars[$matches[1]]['id'])) { 2886 if ($quote) { 2887 return "'" . addcslashes($this->vars[$matches[1]]['id'], "'") . "'"; 2888 } 2889 2890 return $this->vars[$matches[1]]['id']; 2891 } 2892 2893 Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1])); 2894 2895 return '$' . $matches[1]; 2896 }, 2897 $expression 2898 ); 2899 } 2900} 2901