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