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