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