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