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