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(); 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('main') 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(); 1720 1721 if (in_array( 1722 $attributes['ext'], 1723 [ 1724 'GIF', 1725 'JPG', 1726 'PNG', 1727 'SWF', 1728 'PSD', 1729 'BMP', 1730 'TIFF', 1731 'TIFF', 1732 'JPC', 1733 'JP2', 1734 'JPX', 1735 'JB2', 1736 'SWC', 1737 'IFF', 1738 'WBMP', 1739 'XBM', 1740 ] 1741 ) && $mediaobject->canShow() && $mediaobject->fileExists('main') 1742 ) { 1743 if ($width > 0 && $height == 0) { 1744 $perc = $width / $attributes['adjW']; 1745 $height = round($attributes['adjH'] * $perc); 1746 } elseif ($height > 0 && $width == 0) { 1747 $perc = $height / $attributes['adjH']; 1748 $width = round($attributes['adjW'] * $perc); 1749 } else { 1750 $width = $attributes['adjW']; 1751 $height = $attributes['adjH']; 1752 } 1753 $image = $this->report_root->createImageFromObject($mediaobject, $left, $top, $width, $height, $align, $ln); 1754 $this->wt_report->addElement($image); 1755 } 1756 } 1757 } else { 1758 if (file_exists($file) && preg_match('/(jpg|jpeg|png|gif)$/i', $file)) { 1759 $size = getimagesize($file); 1760 if ($width > 0 && $height == 0) { 1761 $perc = $width / $size[0]; 1762 $height = round($size[1] * $perc); 1763 } elseif ($height > 0 && $width == 0) { 1764 $perc = $height / $size[1]; 1765 $width = round($size[0] * $perc); 1766 } else { 1767 $width = $size[0]; 1768 $height = $size[1]; 1769 } 1770 $image = $this->report_root->createImage($file, $left, $top, $width, $height, $align, $ln); 1771 $this->wt_report->addElement($image); 1772 } 1773 } 1774 } 1775 1776 /** 1777 * XML <Line> element handler 1778 * 1779 * @param array $attrs an array of key value pairs for the attributes 1780 */ 1781 private function lineStartHandler($attrs) { 1782 // Start horizontal position, current position (default) 1783 $x1 = '.'; 1784 if (isset($attrs['x1'])) { 1785 if ($attrs['x1'] === '0') { 1786 $x1 = 0; 1787 } elseif ($attrs['x1'] === '.') { 1788 $x1 = '.'; 1789 } elseif (!empty($attrs['x1'])) { 1790 $x1 = (int) $attrs['x1']; 1791 } 1792 } 1793 // Start vertical position, current position (default) 1794 $y1 = '.'; 1795 if (isset($attrs['y1'])) { 1796 if ($attrs['y1'] === '0') { 1797 $y1 = 0; 1798 } elseif ($attrs['y1'] === '.') { 1799 $y1 = '.'; 1800 } elseif (!empty($attrs['y1'])) { 1801 $y1 = (int) $attrs['y1']; 1802 } 1803 } 1804 // End horizontal position, maximum width (default) 1805 $x2 = '.'; 1806 if (isset($attrs['x2'])) { 1807 if ($attrs['x2'] === '0') { 1808 $x2 = 0; 1809 } elseif ($attrs['x2'] === '.') { 1810 $x2 = '.'; 1811 } elseif (!empty($attrs['x2'])) { 1812 $x2 = (int) $attrs['x2']; 1813 } 1814 } 1815 // End vertical position 1816 $y2 = '.'; 1817 if (isset($attrs['y2'])) { 1818 if ($attrs['y2'] === '0') { 1819 $y2 = 0; 1820 } elseif ($attrs['y2'] === '.') { 1821 $y2 = '.'; 1822 } elseif (!empty($attrs['y2'])) { 1823 $y2 = (int) $attrs['y2']; 1824 } 1825 } 1826 1827 $line = $this->report_root->createLine($x1, $y1, $x2, $y2); 1828 $this->wt_report->addElement($line); 1829 } 1830 1831 /** 1832 * XML <List> 1833 * 1834 * @param array $attrs an array of key value pairs for the attributes 1835 */ 1836 private function listStartHandler($attrs) { 1837 global $WT_TREE; 1838 1839 $this->process_repeats++; 1840 if ($this->process_repeats > 1) { 1841 return; 1842 } 1843 1844 $match = []; 1845 if (isset($attrs['sortby'])) { 1846 $sortby = $attrs['sortby']; 1847 if (preg_match("/\\$(\w+)/", $sortby, $match)) { 1848 $sortby = $this->vars[$match[1]]['id']; 1849 $sortby = trim($sortby); 1850 } 1851 } else { 1852 $sortby = 'NAME'; 1853 } 1854 1855 if (isset($attrs['list'])) { 1856 $listname = $attrs['list']; 1857 } else { 1858 $listname = 'individual'; 1859 } 1860 // Some filters/sorts can be applied using SQL, while others require PHP 1861 switch ($listname) { 1862 case 'pending': 1863 $rows = Database::prepare( 1864 "SELECT xref, CASE new_gedcom WHEN '' THEN old_gedcom ELSE new_gedcom END AS gedcom" . 1865 " FROM `##change`" . " WHERE (xref, change_id) IN (" . 1866 " SELECT xref, MAX(change_id)" . 1867 " FROM `##change`" . 1868 " WHERE status = 'pending' AND gedcom_id = :tree_id" . 1869 " GROUP BY xref" . 1870 " )" 1871 )->execute([ 1872 'tree_id' => $WT_TREE->getTreeId(), 1873 ])->fetchAll(); 1874 $this->list = []; 1875 foreach ($rows as $row) { 1876 $this->list[] = GedcomRecord::getInstance($row->xref, $WT_TREE, $row->gedcom); 1877 } 1878 break; 1879 case 'individual': 1880 $sql_select = "SELECT i_id AS xref, i_gedcom AS gedcom FROM `##individuals` "; 1881 $sql_join = ""; 1882 $sql_where = " WHERE i_file = :tree_id"; 1883 $sql_order_by = ""; 1884 $sql_params = ['tree_id' => $WT_TREE->getTreeId()]; 1885 foreach ($attrs as $attr => $value) { 1886 if (strpos($attr, 'filter') === 0 && $value) { 1887 $value = $this->substituteVars($value, false); 1888 // Convert the various filters into SQL 1889 if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) { 1890 $sql_join .= " JOIN `##dates` AS {$attr} ON ({$attr}.d_file=i_file AND {$attr}.d_gid=i_id)"; 1891 $sql_where .= " AND {$attr}.d_fact = :{$attr}fact"; 1892 $sql_params[$attr . 'fact'] = $match[1]; 1893 $date = new Date($match[3]); 1894 if ($match[2] == 'LTE') { 1895 $sql_where .= " AND {$attr}.d_julianday2 <= :{$attr}date"; 1896 $sql_params[$attr . 'date'] = $date->maximumJulianDay(); 1897 } else { 1898 $sql_where .= " AND {$attr}.d_julianday1 >= :{$attr}date"; 1899 $sql_params[$attr . 'date'] = $date->minimumJulianDay(); 1900 } 1901 if ($sortby == $match[1]) { 1902 $sortby = ""; 1903 $sql_order_by .= ($sql_order_by ? ", " : " ORDER BY ") . "{$attr}.d_julianday1"; 1904 } 1905 unset($attrs[$attr]); // This filter has been fully processed 1906 } elseif (preg_match('/^NAME CONTAINS (.*)$/', $value, $match)) { 1907 // Do nothing, unless you have to 1908 if ($match[1] != '' || $sortby == 'NAME') { 1909 $sql_join .= " JOIN `##name` AS {$attr} ON (n_file=i_file AND n_id=i_id)"; 1910 // Search the DB only if there is any name supplied 1911 if ($match[1] != '') { 1912 $names = explode(' ', $match[1]); 1913 foreach ($names as $n => $name) { 1914 $sql_where .= " AND {$attr}.n_full LIKE CONCAT('%', :{$attr}name{$n}, '%')"; 1915 $sql_params[$attr . 'name' . $n] = $name; 1916 } 1917 } 1918 // Let the DB do the name sorting even when no name was entered 1919 if ($sortby == 'NAME') { 1920 $sortby = ''; 1921 $sql_order_by .= ($sql_order_by ? ', ' : ' ORDER BY ') . "{$attr}.n_sort"; 1922 } 1923 } 1924 unset($attrs[$attr]); // This filter has been fully processed 1925 } elseif (preg_match('/^REGEXP \/(.+)\//', $value, $match)) { 1926 $sql_where .= " AND i_gedcom REGEXP :{$attr}gedcom"; 1927 // PDO helpfully escapes backslashes for us, preventing us from matching "\n1 FACT" 1928 $sql_params[$attr . 'gedcom'] = str_replace('\n', "\n", $match[1]); 1929 unset($attrs[$attr]); // This filter has been fully processed 1930 } elseif (preg_match('/^(?:\w+):PLAC CONTAINS (.+)$/', $value, $match)) { 1931 $sql_join .= " JOIN `##places` AS {$attr}a ON ({$attr}a.p_file = i_file)"; 1932 $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)"; 1933 $sql_where .= " AND {$attr}a.p_place LIKE CONCAT('%', :{$attr}place, '%')"; 1934 $sql_params[$attr . 'place'] = $match[1]; 1935 // Don't unset this filter. This is just initial filtering 1936 } elseif (preg_match('/^(\w*):*(\w*) CONTAINS (.+)$/', $value, $match)) { 1937 $sql_where .= " AND i_gedcom LIKE CONCAT('%', :{$attr}contains1, '%', :{$attr}contains2, '%', :{$attr}contains3, '%')"; 1938 $sql_params[$attr . 'contains1'] = $match[1]; 1939 $sql_params[$attr . 'contains2'] = $match[2]; 1940 $sql_params[$attr . 'contains3'] = $match[3]; 1941 // Don't unset this filter. This is just initial filtering 1942 } 1943 } 1944 } 1945 1946 $this->list = []; 1947 $rows = Database::prepare( 1948 $sql_select . $sql_join . $sql_where . $sql_order_by 1949 )->execute($sql_params)->fetchAll(); 1950 1951 foreach ($rows as $row) { 1952 $this->list[$row->xref] = Individual::getInstance($row->xref, $WT_TREE, $row->gedcom); 1953 } 1954 break; 1955 1956 case 'family': 1957 $sql_select = "SELECT f_id AS xref, f_gedcom AS gedcom FROM `##families`"; 1958 $sql_join = ""; 1959 $sql_where = " WHERE f_file = :tree_id"; 1960 $sql_order_by = ""; 1961 $sql_params = ['tree_id' => $WT_TREE->getTreeId()]; 1962 foreach ($attrs as $attr => $value) { 1963 if (strpos($attr, 'filter') === 0 && $value) { 1964 $value = $this->substituteVars($value, false); 1965 // Convert the various filters into SQL 1966 if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) { 1967 $sql_join .= " JOIN `##dates` AS {$attr} ON ({$attr}.d_file=f_file AND {$attr}.d_gid=f_id)"; 1968 $sql_where .= " AND {$attr}.d_fact = :{$attr}fact"; 1969 $sql_params[$attr . 'fact'] = $match[1]; 1970 $date = new Date($match[3]); 1971 if ($match[2] == 'LTE') { 1972 $sql_where .= " AND {$attr}.d_julianday2 <= :{$attr}date"; 1973 $sql_params[$attr . 'date'] = $date->maximumJulianDay(); 1974 } else { 1975 $sql_where .= " AND {$attr}.d_julianday1 >= :{$attr}date"; 1976 $sql_params[$attr . 'date'] = $date->minimumJulianDay(); 1977 } 1978 if ($sortby == $match[1]) { 1979 $sortby = ''; 1980 $sql_order_by .= ($sql_order_by ? ', ' : ' ORDER BY ') . "{$attr}.d_julianday1"; 1981 } 1982 unset($attrs[$attr]); // This filter has been fully processed 1983 } elseif (preg_match('/^REGEXP \/(.+)\//', $value, $match)) { 1984 $sql_where .= " AND f_gedcom REGEXP :{$attr}gedcom"; 1985 // PDO helpfully escapes backslashes for us, preventing us from matching "\n1 FACT" 1986 $sql_params[$attr . 'gedcom'] = str_replace('\n', "\n", $match[1]); 1987 unset($attrs[$attr]); // This filter has been fully processed 1988 } elseif (preg_match('/^NAME CONTAINS (.+)$/', $value, $match)) { 1989 // Do nothing, unless you have to 1990 if ($match[1] != '' || $sortby == 'NAME') { 1991 $sql_join .= " JOIN `##name` AS {$attr} ON n_file = f_file AND n_id IN (f_husb, f_wife)"; 1992 // Search the DB only if there is any name supplied 1993 if ($match[1] != '') { 1994 $names = explode(' ', $match[1]); 1995 foreach ($names as $n => $name) { 1996 $sql_where .= " AND {$attr}.n_full LIKE CONCAT('%', :{$attr}name{$n}, '%')"; 1997 $sql_params[$attr . 'name' . $n] = $name; 1998 } 1999 } 2000 // Let the DB do the name sorting even when no name was entered 2001 if ($sortby == 'NAME') { 2002 $sortby = ''; 2003 $sql_order_by .= ($sql_order_by ? ', ' : ' ORDER BY ') . "{$attr}.n_sort"; 2004 } 2005 } 2006 unset($attrs[$attr]); // This filter has been fully processed 2007 2008 } elseif (preg_match('/^(?:\w+):PLAC CONTAINS (.+)$/', $value, $match)) { 2009 $sql_join .= " JOIN `##places` AS {$attr}a ON ({$attr}a.p_file=f_file)"; 2010 $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)"; 2011 $sql_where .= " AND {$attr}a.p_place LIKE CONCAT('%', :{$attr}place, '%')"; 2012 $sql_params[$attr . 'place'] = $match[1]; 2013 // Don't unset this filter. This is just initial filtering 2014 } elseif (preg_match('/^(\w*):*(\w*) CONTAINS (.+)$/', $value, $match)) { 2015 $sql_where .= " AND f_gedcom LIKE CONCAT('%', :{$attr}contains1, '%', :{$attr}contains2, '%', :{$attr}contains3, '%')"; 2016 $sql_params[$attr . 'contains1'] = $match[1]; 2017 $sql_params[$attr . 'contains2'] = $match[2]; 2018 $sql_params[$attr . 'contains3'] = $match[3]; 2019 // Don't unset this filter. This is just initial filtering 2020 } 2021 } 2022 } 2023 2024 $this->list = []; 2025 $rows = Database::prepare( 2026 $sql_select . $sql_join . $sql_where . $sql_order_by 2027 )->execute($sql_params)->fetchAll(); 2028 2029 foreach ($rows as $row) { 2030 $this->list[$row->xref] = Family::getInstance($row->xref, $WT_TREE, $row->gedcom); 2031 } 2032 break; 2033 2034 default: 2035 throw new \DomainException('Invalid list name: ' . $listname); 2036 } 2037 2038 $filters = []; 2039 $filters2 = []; 2040 if (isset($attrs['filter1']) && count($this->list) > 0) { 2041 foreach ($attrs as $key => $value) { 2042 if (preg_match("/filter(\d)/", $key)) { 2043 $condition = $value; 2044 if (preg_match("/@(\w+)/", $condition, $match)) { 2045 $id = $match[1]; 2046 $value = "''"; 2047 if ($id == 'ID') { 2048 if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) { 2049 $value = "'" . $match[1] . "'"; 2050 } 2051 } elseif ($id == 'fact') { 2052 $value = "'" . $this->fact . "'"; 2053 } elseif ($id == 'desc') { 2054 $value = "'" . $this->desc . "'"; 2055 } else { 2056 if (preg_match("/\d $id (.+)/", $this->gedrec, $match)) { 2057 $value = "'" . str_replace('@', '', trim($match[1])) . "'"; 2058 } 2059 } 2060 $condition = preg_replace("/@$id/", $value, $condition); 2061 } 2062 //-- handle regular expressions 2063 if (preg_match("/([A-Z:]+)\s*([^\s]+)\s*(.+)/", $condition, $match)) { 2064 $tag = trim($match[1]); 2065 $expr = trim($match[2]); 2066 $val = trim($match[3]); 2067 if (preg_match("/\\$(\w+)/", $val, $match)) { 2068 $val = $this->vars[$match[1]]['id']; 2069 $val = trim($val); 2070 } 2071 if ($val) { 2072 $searchstr = ''; 2073 $tags = explode(':', $tag); 2074 //-- only limit to a level number if we are specifically looking at a level 2075 if (count($tags) > 1) { 2076 $level = 1; 2077 foreach ($tags as $t) { 2078 if (!empty($searchstr)) { 2079 $searchstr .= "[^\n]*(\n[2-9][^\n]*)*\n"; 2080 } 2081 //-- search for both EMAIL and _EMAIL... silly double gedcom standard 2082 if ($t == 'EMAIL' || $t == '_EMAIL') { 2083 $t = '_?EMAIL'; 2084 } 2085 $searchstr .= $level . ' ' . $t; 2086 $level++; 2087 } 2088 } else { 2089 if ($tag == 'EMAIL' || $tag == '_EMAIL') { 2090 $tag = '_?EMAIL'; 2091 } 2092 $t = $tag; 2093 $searchstr = '1 ' . $tag; 2094 } 2095 switch ($expr) { 2096 case 'CONTAINS': 2097 if ($t == 'PLAC') { 2098 $searchstr .= "[^\n]*[, ]*" . $val; 2099 } else { 2100 $searchstr .= "[^\n]*" . $val; 2101 } 2102 $filters[] = $searchstr; 2103 break; 2104 default: 2105 $filters2[] = ['tag' => $tag, 'expr' => $expr, 'val' => $val]; 2106 break; 2107 } 2108 } 2109 } 2110 } 2111 } 2112 } 2113 //-- apply other filters to the list that could not be added to the search string 2114 if ($filters) { 2115 foreach ($this->list as $key => $record) { 2116 foreach ($filters as $filter) { 2117 if (!preg_match('/' . $filter . '/i', $record->privatizeGedcom(Auth::accessLevel($WT_TREE)))) { 2118 unset($this->list[$key]); 2119 break; 2120 } 2121 } 2122 } 2123 } 2124 if ($filters2) { 2125 $mylist = []; 2126 foreach ($this->list as $indi) { 2127 $key = $indi->getXref(); 2128 $grec = $indi->privatizeGedcom(Auth::accessLevel($WT_TREE)); 2129 $keep = true; 2130 foreach ($filters2 as $filter) { 2131 if ($keep) { 2132 $tag = $filter['tag']; 2133 $expr = $filter['expr']; 2134 $val = $filter['val']; 2135 if ($val == "''") { 2136 $val = ''; 2137 } 2138 $tags = explode(':', $tag); 2139 $t = end($tags); 2140 $v = $this->getGedcomValue($tag, 1, $grec); 2141 //-- check for EMAIL and _EMAIL (silly double gedcom standard :P) 2142 if ($t == 'EMAIL' && empty($v)) { 2143 $tag = str_replace('EMAIL', '_EMAIL', $tag); 2144 $tags = explode(':', $tag); 2145 $t = end($tags); 2146 $v = Functions::getSubRecord(1, $tag, $grec); 2147 } 2148 2149 switch ($expr) { 2150 case 'GTE': 2151 if ($t == 'DATE') { 2152 $date1 = new Date($v); 2153 $date2 = new Date($val); 2154 $keep = (Date::compare($date1, $date2) >= 0); 2155 } elseif ($val >= $v) { 2156 $keep = true; 2157 } 2158 break; 2159 case 'LTE': 2160 if ($t == 'DATE') { 2161 $date1 = new Date($v); 2162 $date2 = new Date($val); 2163 $keep = (Date::compare($date1, $date2) <= 0); 2164 } elseif ($val >= $v) { 2165 $keep = true; 2166 } 2167 break; 2168 default: 2169 if ($v == $val) { 2170 $keep = true; 2171 } else { 2172 $keep = false; 2173 } 2174 break; 2175 } 2176 } 2177 } 2178 if ($keep) { 2179 $mylist[$key] = $indi; 2180 } 2181 } 2182 $this->list = $mylist; 2183 } 2184 2185 switch ($sortby) { 2186 case 'NAME': 2187 uasort($this->list, '\Fisharebest\Webtrees\GedcomRecord::compare'); 2188 break; 2189 case 'CHAN': 2190 uasort($this->list, function (GedcomRecord $x, GedcomRecord $y) { 2191 return $y->lastChangeTimestamp(true) - $x->lastChangeTimestamp(true); 2192 }); 2193 break; 2194 case 'BIRT:DATE': 2195 uasort($this->list, '\Fisharebest\Webtrees\Individual::compareBirthDate'); 2196 break; 2197 case 'DEAT:DATE': 2198 uasort($this->list, '\Fisharebest\Webtrees\Individual::compareDeathDate'); 2199 break; 2200 case 'MARR:DATE': 2201 uasort($this->list, '\Fisharebest\Webtrees\Family::compareMarrDate'); 2202 break; 2203 default: 2204 // unsorted or already sorted by SQL 2205 break; 2206 } 2207 2208 array_push($this->repeats_stack, [$this->repeats, $this->repeat_bytes]); 2209 $this->repeat_bytes = xml_get_current_line_number($this->parser) + 1; 2210 } 2211 2212 /** 2213 * XML <List> 2214 */ 2215 private function listEndHandler() { 2216 global $report; 2217 2218 $this->process_repeats--; 2219 if ($this->process_repeats > 0) { 2220 return; 2221 } 2222 2223 // Check if there is any list 2224 if (count($this->list) > 0) { 2225 $lineoffset = 0; 2226 foreach ($this->repeats_stack as $rep) { 2227 $lineoffset += $rep[1]; 2228 } 2229 //-- read the xml from the file 2230 $lines = file($report); 2231 while ((strpos($lines[$lineoffset + $this->repeat_bytes], '<List') === false) && (($lineoffset + $this->repeat_bytes) > 0)) { 2232 $lineoffset--; 2233 } 2234 $lineoffset++; 2235 $reportxml = "<tempdoc>\n"; 2236 $line_nr = $lineoffset + $this->repeat_bytes; 2237 // List Level counter 2238 $count = 1; 2239 while (0 < $count) { 2240 if (strpos($lines[$line_nr], '<List') !== false) { 2241 $count++; 2242 } elseif (strpos($lines[$line_nr], '</List') !== false) { 2243 $count--; 2244 } 2245 if (0 < $count) { 2246 $reportxml .= $lines[$line_nr]; 2247 } 2248 $line_nr++; 2249 } 2250 // No need to drag this 2251 unset($lines); 2252 $reportxml .= '</tempdoc>'; 2253 // Save original values 2254 array_push($this->parser_stack, $this->parser); 2255 $oldgedrec = $this->gedrec; 2256 2257 $this->list_total = count($this->list); 2258 $this->list_private = 0; 2259 foreach ($this->list as $record) { 2260 if ($record->canShow()) { 2261 $this->gedrec = $record->privatizeGedcom(Auth::accessLevel($record->getTree())); 2262 //-- start the sax parser 2263 $repeat_parser = xml_parser_create(); 2264 $this->parser = $repeat_parser; 2265 xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false); 2266 xml_set_element_handler($repeat_parser, [$this, 'startElement'], [$this, 'endElement']); 2267 xml_set_character_data_handler($repeat_parser, [$this, 'characterData']); 2268 if (!xml_parse($repeat_parser, $reportxml, true)) { 2269 throw new \DomainException(sprintf( 2270 'ListEHandler XML error: %s at line %d', 2271 xml_error_string(xml_get_error_code($repeat_parser)), 2272 xml_get_current_line_number($repeat_parser) 2273 )); 2274 } 2275 xml_parser_free($repeat_parser); 2276 } else { 2277 $this->list_private++; 2278 } 2279 } 2280 $this->list = []; 2281 $this->parser = array_pop($this->parser_stack); 2282 $this->gedrec = $oldgedrec; 2283 } 2284 list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack); 2285 } 2286 2287 /** 2288 * XML <ListTotal> element handler 2289 * 2290 * Prints the total number of records in a list 2291 * The total number is collected from 2292 * List and Relatives 2293 */ 2294 private function listTotalStartHandler() { 2295 if ($this->list_private == 0) { 2296 $this->current_element->addText($this->list_total); 2297 } else { 2298 $this->current_element->addText(($this->list_total - $this->list_private) . ' / ' . $this->list_total); 2299 } 2300 } 2301 2302 /** 2303 * XML <Relatives> 2304 * 2305 * @param array $attrs an array of key value pairs for the attributes 2306 */ 2307 private function relativesStartHandler($attrs) { 2308 global $WT_TREE; 2309 2310 $this->process_repeats++; 2311 if ($this->process_repeats > 1) { 2312 return; 2313 } 2314 2315 $sortby = 'NAME'; 2316 if (isset($attrs['sortby'])) { 2317 $sortby = $attrs['sortby']; 2318 } 2319 $match = []; 2320 if (preg_match("/\\$(\w+)/", $sortby, $match)) { 2321 $sortby = $this->vars[$match[1]]['id']; 2322 $sortby = trim($sortby); 2323 } 2324 2325 $maxgen = -1; 2326 if (isset($attrs['maxgen'])) { 2327 $maxgen = $attrs['maxgen']; 2328 } 2329 if ($maxgen == '*') { 2330 $maxgen = -1; 2331 } 2332 2333 $group = 'child-family'; 2334 if (isset($attrs['group'])) { 2335 $group = $attrs['group']; 2336 } 2337 if (preg_match("/\\$(\w+)/", $group, $match)) { 2338 $group = $this->vars[$match[1]]['id']; 2339 $group = trim($group); 2340 } 2341 2342 $id = ''; 2343 if (isset($attrs['id'])) { 2344 $id = $attrs['id']; 2345 } 2346 if (preg_match("/\\$(\w+)/", $id, $match)) { 2347 $id = $this->vars[$match[1]]['id']; 2348 $id = trim($id); 2349 } 2350 2351 $this->list = []; 2352 $person = Individual::getInstance($id, $WT_TREE); 2353 if (!empty($person)) { 2354 $this->list[$id] = $person; 2355 switch ($group) { 2356 case 'child-family': 2357 foreach ($person->getChildFamilies() as $family) { 2358 $husband = $family->getHusband(); 2359 $wife = $family->getWife(); 2360 if (!empty($husband)) { 2361 $this->list[$husband->getXref()] = $husband; 2362 } 2363 if (!empty($wife)) { 2364 $this->list[$wife->getXref()] = $wife; 2365 } 2366 $children = $family->getChildren(); 2367 foreach ($children as $child) { 2368 if (!empty($child)) { 2369 $this->list[$child->getXref()] = $child; 2370 } 2371 } 2372 } 2373 break; 2374 case 'spouse-family': 2375 foreach ($person->getSpouseFamilies() as $family) { 2376 $husband = $family->getHusband(); 2377 $wife = $family->getWife(); 2378 if (!empty($husband)) { 2379 $this->list[$husband->getXref()] = $husband; 2380 } 2381 if (!empty($wife)) { 2382 $this->list[$wife->getXref()] = $wife; 2383 } 2384 $children = $family->getChildren(); 2385 foreach ($children as $child) { 2386 if (!empty($child)) { 2387 $this->list[$child->getXref()] = $child; 2388 } 2389 } 2390 } 2391 break; 2392 case 'direct-ancestors': 2393 $this->addAncestors($this->list, $id, false, $maxgen); 2394 break; 2395 case 'ancestors': 2396 $this->addAncestors($this->list, $id, true, $maxgen); 2397 break; 2398 case 'descendants': 2399 $this->list[$id]->generation = 1; 2400 $this->addDescendancy($this->list, $id, false, $maxgen); 2401 break; 2402 case 'all': 2403 $this->addAncestors($this->list, $id, true, $maxgen); 2404 $this->addDescendancy($this->list, $id, true, $maxgen); 2405 break; 2406 } 2407 } 2408 2409 switch ($sortby) { 2410 case 'NAME': 2411 uasort($this->list, '\Fisharebest\Webtrees\GedcomRecord::compare'); 2412 break; 2413 case 'BIRT:DATE': 2414 uasort($this->list, '\Fisharebest\Webtrees\Individual::compareBirthDate'); 2415 break; 2416 case 'DEAT:DATE': 2417 uasort($this->list, '\Fisharebest\Webtrees\Individual::compareDeathDate'); 2418 break; 2419 case 'generation': 2420 $newarray = []; 2421 reset($this->list); 2422 $genCounter = 1; 2423 while (count($newarray) < count($this->list)) { 2424 foreach ($this->list as $key => $value) { 2425 $this->generation = $value->generation; 2426 if ($this->generation == $genCounter) { 2427 $newarray[$key] = new \stdClass; 2428 $newarray[$key]->generation = $this->generation; 2429 } 2430 } 2431 $genCounter++; 2432 } 2433 $this->list = $newarray; 2434 break; 2435 default: 2436 // unsorted 2437 break; 2438 } 2439 array_push($this->repeats_stack, [$this->repeats, $this->repeat_bytes]); 2440 $this->repeat_bytes = xml_get_current_line_number($this->parser) + 1; 2441 } 2442 2443 /** 2444 * XML </ Relatives> 2445 */ 2446 private function relativesEndHandler() { 2447 global $report, $WT_TREE; 2448 2449 $this->process_repeats--; 2450 if ($this->process_repeats > 0) { 2451 return; 2452 } 2453 2454 // Check if there is any relatives 2455 if (count($this->list) > 0) { 2456 $lineoffset = 0; 2457 foreach ($this->repeats_stack as $rep) { 2458 $lineoffset += $rep[1]; 2459 } 2460 //-- read the xml from the file 2461 $lines = file($report); 2462 while ((strpos($lines[$lineoffset + $this->repeat_bytes], '<Relatives') === false) && (($lineoffset + $this->repeat_bytes) > 0)) { 2463 $lineoffset--; 2464 } 2465 $lineoffset++; 2466 $reportxml = "<tempdoc>\n"; 2467 $line_nr = $lineoffset + $this->repeat_bytes; 2468 // Relatives Level counter 2469 $count = 1; 2470 while (0 < $count) { 2471 if (strpos($lines[$line_nr], '<Relatives') !== false) { 2472 $count++; 2473 } elseif (strpos($lines[$line_nr], '</Relatives') !== false) { 2474 $count--; 2475 } 2476 if (0 < $count) { 2477 $reportxml .= $lines[$line_nr]; 2478 } 2479 $line_nr++; 2480 } 2481 // No need to drag this 2482 unset($lines); 2483 $reportxml .= "</tempdoc>\n"; 2484 // Save original values 2485 array_push($this->parser_stack, $this->parser); 2486 $oldgedrec = $this->gedrec; 2487 2488 $this->list_total = count($this->list); 2489 $this->list_private = 0; 2490 foreach ($this->list as $key => $value) { 2491 if (isset($value->generation)) { 2492 $this->generation = $value->generation; 2493 } 2494 $tmp = GedcomRecord::getInstance($key, $WT_TREE); 2495 $this->gedrec = $tmp->privatizeGedcom(Auth::accessLevel($WT_TREE)); 2496 2497 $repeat_parser = xml_parser_create(); 2498 $this->parser = $repeat_parser; 2499 xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false); 2500 xml_set_element_handler($repeat_parser, [$this, 'startElement'], [$this, 'endElement']); 2501 xml_set_character_data_handler($repeat_parser, [$this, 'characterData']); 2502 2503 if (!xml_parse($repeat_parser, $reportxml, true)) { 2504 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))); 2505 } 2506 xml_parser_free($repeat_parser); 2507 } 2508 // Clean up the list array 2509 $this->list = []; 2510 $this->parser = array_pop($this->parser_stack); 2511 $this->gedrec = $oldgedrec; 2512 } 2513 list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack); 2514 } 2515 2516 /** 2517 * XML <Generation /> element handler 2518 * 2519 * Prints the number of generations 2520 */ 2521 private function generationStartHandler() { 2522 $this->current_element->addText($this->generation); 2523 } 2524 2525 /** 2526 * XML <NewPage /> element handler 2527 * 2528 * Has to be placed in an element (header, pageheader, body or footer) 2529 */ 2530 private function newPageStartHandler() { 2531 $temp = 'addpage'; 2532 $this->wt_report->addElement($temp); 2533 } 2534 2535 /** 2536 * XML <html> 2537 * 2538 * @param string $tag HTML tag name 2539 * @param array[] $attrs an array of key value pairs for the attributes 2540 */ 2541 private function htmlStartHandler($tag, $attrs) { 2542 if ($tag === 'tempdoc') { 2543 return; 2544 } 2545 array_push($this->wt_report_stack, $this->wt_report); 2546 $this->wt_report = $this->report_root->createHTML($tag, $attrs); 2547 $this->current_element = $this->wt_report; 2548 2549 array_push($this->print_data_stack, $this->print_data); 2550 $this->print_data = true; 2551 } 2552 2553 /** 2554 * XML </html> 2555 * 2556 * @param string $tag 2557 */ 2558 private function htmlEndHandler($tag) { 2559 if ($tag === 'tempdoc') { 2560 return; 2561 } 2562 2563 $this->print_data = array_pop($this->print_data_stack); 2564 $this->current_element = $this->wt_report; 2565 $this->wt_report = array_pop($this->wt_report_stack); 2566 if (!is_null($this->wt_report)) { 2567 $this->wt_report->addElement($this->current_element); 2568 } else { 2569 $this->wt_report = $this->current_element; 2570 } 2571 } 2572 2573 /** 2574 * Handle <Input> 2575 */ 2576 private function inputStartHandler() { 2577 // Dummy function, to prevent the default HtmlStartHandler() being called 2578 } 2579 2580 /** 2581 * Handle </Input> 2582 */ 2583 private function inputEndHandler() { 2584 // Dummy function, to prevent the default HtmlEndHandler() being called 2585 } 2586 2587 /** 2588 * Handle <Report> 2589 */ 2590 private function reportStartHandler() { 2591 // Dummy function, to prevent the default HtmlStartHandler() being called 2592 } 2593 2594 /** 2595 * Handle </Report> 2596 */ 2597 private function reportEndHandler() { 2598 // Dummy function, to prevent the default HtmlEndHandler() being called 2599 } 2600 2601 /** 2602 * XML </titleEndHandler> 2603 */ 2604 private function titleEndHandler() { 2605 $this->report_root->addTitle($this->text); 2606 } 2607 2608 /** 2609 * XML </descriptionEndHandler> 2610 */ 2611 private function descriptionEndHandler() { 2612 $this->report_root->addDescription($this->text); 2613 } 2614 2615 /** 2616 * Create a list of all descendants. 2617 * 2618 * @param string[] $list 2619 * @param string $pid 2620 * @param bool $parents 2621 * @param int $generations 2622 */ 2623 private function addDescendancy(&$list, $pid, $parents = false, $generations = -1) { 2624 global $WT_TREE; 2625 2626 $person = Individual::getInstance($pid, $WT_TREE); 2627 if ($person === null) { 2628 return; 2629 } 2630 if (!isset($list[$pid])) { 2631 $list[$pid] = $person; 2632 } 2633 if (!isset($list[$pid]->generation)) { 2634 $list[$pid]->generation = 0; 2635 } 2636 foreach ($person->getSpouseFamilies() as $family) { 2637 if ($parents) { 2638 $husband = $family->getHusband(); 2639 $wife = $family->getWife(); 2640 if ($husband) { 2641 $list[$husband->getXref()] = $husband; 2642 if (isset($list[$pid]->generation)) { 2643 $list[$husband->getXref()]->generation = $list[$pid]->generation - 1; 2644 } else { 2645 $list[$husband->getXref()]->generation = 1; 2646 } 2647 } 2648 if ($wife) { 2649 $list[$wife->getXref()] = $wife; 2650 if (isset($list[$pid]->generation)) { 2651 $list[$wife->getXref()]->generation = $list[$pid]->generation - 1; 2652 } else { 2653 $list[$wife->getXref()]->generation = 1; 2654 } 2655 } 2656 } 2657 $children = $family->getChildren(); 2658 foreach ($children as $child) { 2659 if ($child) { 2660 $list[$child->getXref()] = $child; 2661 if (isset($list[$pid]->generation)) { 2662 $list[$child->getXref()]->generation = $list[$pid]->generation + 1; 2663 } else { 2664 $list[$child->getXref()]->generation = 2; 2665 } 2666 } 2667 } 2668 if ($generations == -1 || $list[$pid]->generation + 1 < $generations) { 2669 foreach ($children as $child) { 2670 $this->addDescendancy($list, $child->getXref(), $parents, $generations); // recurse on the childs family 2671 } 2672 } 2673 } 2674 } 2675 2676 /** 2677 * Create a list of all ancestors. 2678 * 2679 * @param string[] $list 2680 * @param string $pid 2681 * @param bool $children 2682 * @param int $generations 2683 */ 2684 private function addAncestors(&$list, $pid, $children = false, $generations = -1) { 2685 global $WT_TREE; 2686 2687 $genlist = [$pid]; 2688 $list[$pid]->generation = 1; 2689 while (count($genlist) > 0) { 2690 $id = array_shift($genlist); 2691 if (strpos($id, 'empty') === 0) { 2692 continue; // id can be something like “empty7” 2693 } 2694 $person = Individual::getInstance($id, $WT_TREE); 2695 foreach ($person->getChildFamilies() as $family) { 2696 $husband = $family->getHusband(); 2697 $wife = $family->getWife(); 2698 if ($husband) { 2699 $list[$husband->getXref()] = $husband; 2700 $list[$husband->getXref()]->generation = $list[$id]->generation + 1; 2701 } 2702 if ($wife) { 2703 $list[$wife->getXref()] = $wife; 2704 $list[$wife->getXref()]->generation = $list[$id]->generation + 1; 2705 } 2706 if ($generations == -1 || $list[$id]->generation + 1 < $generations) { 2707 if ($husband) { 2708 array_push($genlist, $husband->getXref()); 2709 } 2710 if ($wife) { 2711 array_push($genlist, $wife->getXref()); 2712 } 2713 } 2714 if ($children) { 2715 foreach ($family->getChildren() as $child) { 2716 $list[$child->getXref()] = $child; 2717 if (isset($list[$id]->generation)) { 2718 $list[$child->getXref()]->generation = $list[$id]->generation; 2719 } else { 2720 $list[$child->getXref()]->generation = 1; 2721 } 2722 } 2723 } 2724 } 2725 } 2726 } 2727 2728 /** 2729 * get gedcom tag value 2730 * 2731 * @param string $tag The tag to find, use : to delineate subtags 2732 * @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 2733 * @param string $gedrec The gedcom record to get the value from 2734 * 2735 * @return string the value of a gedcom tag from the given gedcom record 2736 */ 2737 private function getGedcomValue($tag, $level, $gedrec) { 2738 global $WT_TREE; 2739 2740 if (empty($gedrec)) { 2741 return ''; 2742 } 2743 $tags = explode(':', $tag); 2744 $origlevel = $level; 2745 if ($level == 0) { 2746 $level = $gedrec[0] + 1; 2747 } 2748 2749 $subrec = $gedrec; 2750 foreach ($tags as $t) { 2751 $lastsubrec = $subrec; 2752 $subrec = Functions::getSubRecord($level, "$level $t", $subrec); 2753 if (empty($subrec) && $origlevel == 0) { 2754 $level--; 2755 $subrec = Functions::getSubRecord($level, "$level $t", $lastsubrec); 2756 } 2757 if (empty($subrec)) { 2758 if ($t == 'TITL') { 2759 $subrec = Functions::getSubRecord($level, "$level ABBR", $lastsubrec); 2760 if (!empty($subrec)) { 2761 $t = 'ABBR'; 2762 } 2763 } 2764 if (empty($subrec)) { 2765 if ($level > 0) { 2766 $level--; 2767 } 2768 $subrec = Functions::getSubRecord($level, "@ $t", $gedrec); 2769 if (empty($subrec)) { 2770 return ''; 2771 } 2772 } 2773 } 2774 $level++; 2775 } 2776 $level--; 2777 $ct = preg_match("/$level $t(.*)/", $subrec, $match); 2778 if ($ct == 0) { 2779 $ct = preg_match("/$level @.+@ (.+)/", $subrec, $match); 2780 } 2781 if ($ct == 0) { 2782 $ct = preg_match("/@ $t (.+)/", $subrec, $match); 2783 } 2784 if ($ct > 0) { 2785 $value = trim($match[1]); 2786 if ($t == 'NOTE' && preg_match('/^@(.+)@$/', $value, $match)) { 2787 $note = Note::getInstance($match[1], $WT_TREE); 2788 if ($note) { 2789 $value = $note->getNote(); 2790 } else { 2791 //-- set the value to the id without the @ 2792 $value = $match[1]; 2793 } 2794 } 2795 if ($level != 0 || $t != 'NOTE') { 2796 $value .= Functions::getCont($level + 1, $subrec); 2797 } 2798 2799 return $value; 2800 } 2801 2802 return ''; 2803 } 2804 2805 /** 2806 * Replace variable identifiers with their values. 2807 * 2808 * @param string $expression An expression such as "$foo == 123" 2809 * @param bool $quote Whether to add quotation marks 2810 * 2811 * @return string 2812 */ 2813 private function substituteVars($expression, $quote) { 2814 return preg_replace_callback( 2815 '/\$(\w+)/', 2816 function ($matches) use ($quote) { 2817 if (isset($this->vars[$matches[1]]['id'])) { 2818 if ($quote) { 2819 return "'" . addcslashes($this->vars[$matches[1]]['id'], "'") . "'"; 2820 } else { 2821 return $this->vars[$matches[1]]['id']; 2822 } 2823 } else { 2824 Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1])); 2825 2826 return '$' . $matches[1]; 2827 } 2828 }, 2829 $expression 2830 ); 2831 } 2832} 2833