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