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