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