1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2016 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 /** @todo This attribute is public to support the PHP5.3 closure workaround. */ 114 /** @var string[][] Variables defined in the report at run-time */ 115 public $vars; 116 117 /** 118 * Create a parser for a report 119 * 120 * @param string $report The XML filename 121 * @param ReportBase $report_root 122 * @param string[][] $vars 123 */ 124 public function __construct($report, ReportBase $report_root = null, array $vars = []) { 125 $this->report_root = $report_root; 126 $this->wt_report = $report_root; 127 $this->current_element = new ReportBaseElement; 128 $this->vars = $vars; 129 parent::__construct($report); 130 } 131 132 /** 133 * XML start element handler 134 * 135 * This function is called whenever a starting element is reached 136 * The element handler will be called if found, otherwise it must be HTML 137 * 138 * @param resource $parser the resource handler for the XML parser 139 * @param string $name the name of the XML element parsed 140 * @param array $attrs an array of key value pairs for the attributes 141 */ 142 protected function startElement($parser, $name, $attrs) { 143 $newattrs = []; 144 145 foreach ($attrs as $key => $value) { 146 if (preg_match("/^\\$(\w+)$/", $value, $match)) { 147 if ((isset($this->vars[$match[1]]['id'])) && (!isset($this->vars[$match[1]]['gedcom']))) { 148 $value = $this->vars[$match[1]]['id']; 149 } 150 } 151 $newattrs[$key] = $value; 152 } 153 $attrs = $newattrs; 154 if ($this->process_footnote && ($this->process_ifs === 0 || $name === "if") && ($this->process_gedcoms === 0 || $name === "Gedcom") && ($this->process_repeats === 0 || $name === "Facts" || $name === "RepeatTag")) { 155 $start_method = $name . 'StartHandler'; 156 $end_method = $name . 'EndHandler'; 157 if (method_exists($this, $start_method)) { 158 $this->$start_method($attrs); 159 } elseif (!method_exists($this, $end_method)) { 160 $this->htmlStartHandler($name, $attrs); 161 } 162 } 163 } 164 165 /** 166 * XML end element handler 167 * 168 * This function is called whenever an ending element is reached 169 * The element handler will be called if found, otherwise it must be HTML 170 * 171 * @param resource $parser the resource handler for the XML parser 172 * @param string $name the name of the XML element parsed 173 */ 174 protected function endElement($parser, $name) { 175 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")) { 176 $start_method = $name . 'StartHandler'; 177 $end_method = $name . 'EndHandler'; 178 if (method_exists($this, $end_method)) { 179 $this->$end_method(); 180 } elseif (!method_exists($this, $start_method)) { 181 $this->htmlEndHandler($name); 182 } 183 } 184 } 185 186 /** 187 * XML character data handler 188 * 189 * @param resource $parser the resource handler for the XML parser 190 * @param string $data the name of the XML element parsed 191 */ 192 protected function characterData($parser, $data) { 193 if ($this->print_data && $this->process_gedcoms === 0 && $this->process_ifs === 0 && $this->process_repeats === 0) { 194 $this->current_element->addText($data); 195 } 196 } 197 198 /** 199 * XML <style> 200 * 201 * @param array $attrs an array of key value pairs for the attributes 202 */ 203 private function styleStartHandler($attrs) { 204 if (empty($attrs['name'])) { 205 throw new \DomainException('REPORT ERROR Style: The "name" of the style is missing or not set in the XML file.'); 206 } 207 208 // array Style that will be passed on 209 $s = []; 210 211 // string Name af the style 212 $s['name'] = $attrs['name']; 213 214 // string Name of the DEFAULT font 215 $s['font'] = $this->wt_report->defaultFont; 216 if (!empty($attrs['font'])) { 217 $s['font'] = $attrs['font']; 218 } 219 220 // int The size of the font in points 221 $s['size'] = $this->wt_report->defaultFontSize; 222 if (!empty($attrs['size'])) { 223 $s['size'] = (int) $attrs['size']; 224 } // Get it as int to ignore all decimal points or text (if any text then int(0)) 225 226 // string B: bold, I: italic, U: underline, D: line trough, The default value is regular. 227 $s['style'] = ""; 228 if (!empty($attrs['style'])) { 229 $s['style'] = $attrs['style']; 230 } 231 232 $this->wt_report->addStyle($s); 233 } 234 235 /** 236 * XML <Doc> 237 * 238 * Sets up the basics of the document proparties 239 * 240 * @param array $attrs an array of key value pairs for the attributes 241 */ 242 private function docStartHandler($attrs) { 243 $this->parser = $this->xml_parser; 244 245 // Custom page width 246 if (!empty($attrs['customwidth'])) { 247 $this->wt_report->pagew = (int) $attrs['customwidth']; 248 } // Get it as int to ignore all decimal points or text (if any text then int(0)) 249 // Custom Page height 250 if (!empty($attrs['customheight'])) { 251 $this->wt_report->pageh = (int) $attrs['customheight']; 252 } // Get it as int to ignore all decimal points or text (if any text then int(0)) 253 254 // Left Margin 255 if (isset($attrs['leftmargin'])) { 256 if ($attrs['leftmargin'] === "0") { 257 $this->wt_report->leftmargin = 0; 258 } elseif (!empty($attrs['leftmargin'])) { 259 $this->wt_report->leftmargin = (int) $attrs['leftmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0)) 260 } 261 } 262 // Right Margin 263 if (isset($attrs['rightmargin'])) { 264 if ($attrs['rightmargin'] === "0") { 265 $this->wt_report->rightmargin = 0; 266 } elseif (!empty($attrs['rightmargin'])) { 267 $this->wt_report->rightmargin = (int) $attrs['rightmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0)) 268 } 269 } 270 // Top Margin 271 if (isset($attrs['topmargin'])) { 272 if ($attrs['topmargin'] === "0") { 273 $this->wt_report->topmargin = 0; 274 } elseif (!empty($attrs['topmargin'])) { 275 $this->wt_report->topmargin = (int) $attrs['topmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0)) 276 } 277 } 278 // Bottom Margin 279 if (isset($attrs['bottommargin'])) { 280 if ($attrs['bottommargin'] === "0") { 281 $this->wt_report->bottommargin = 0; 282 } elseif (!empty($attrs['bottommargin'])) { 283 $this->wt_report->bottommargin = (int) $attrs['bottommargin']; // Get it as int to ignore all decimal points or text (if any text then int(0)) 284 } 285 } 286 // Header Margin 287 if (isset($attrs['headermargin'])) { 288 if ($attrs['headermargin'] === "0") { 289 $this->wt_report->headermargin = 0; 290 } elseif (!empty($attrs['headermargin'])) { 291 $this->wt_report->headermargin = (int) $attrs['headermargin']; // Get it as int to ignore all decimal points or text (if any text then int(0)) 292 } 293 } 294 // Footer Margin 295 if (isset($attrs['footermargin'])) { 296 if ($attrs['footermargin'] === "0") { 297 $this->wt_report->footermargin = 0; 298 } elseif (!empty($attrs['footermargin'])) { 299 $this->wt_report->footermargin = (int) $attrs['footermargin']; // Get it as int to ignore all decimal points or text (if any text then int(0)) 300 } 301 } 302 303 // Page Orientation 304 if (!empty($attrs['orientation'])) { 305 if ($attrs['orientation'] == "landscape") { 306 $this->wt_report->orientation = "landscape"; 307 } elseif ($attrs['orientation'] == "portrait") { 308 $this->wt_report->orientation = "portrait"; 309 } 310 } 311 // Page Size 312 if (!empty($attrs['pageSize'])) { 313 $this->wt_report->pageFormat = strtoupper($attrs['pageSize']); 314 } 315 316 // Show Generated By... 317 if (isset($attrs['showGeneratedBy'])) { 318 if ($attrs['showGeneratedBy'] === "0") { 319 $this->wt_report->showGenText = false; 320 } elseif ($attrs['showGeneratedBy'] === "1") { 321 $this->wt_report->showGenText = true; 322 } 323 } 324 325 $this->wt_report->setup(); 326 } 327 328 /** 329 * XML </Doc> 330 */ 331 private function docEndHandler() { 332 $this->wt_report->run(); 333 } 334 335 /** 336 * XML <Header> 337 */ 338 private function headerStartHandler() { 339 // Clear the Header before any new elements are added 340 $this->wt_report->clearHeader(); 341 $this->wt_report->setProcessing("H"); 342 } 343 344 /** 345 * XML <PageHeader> 346 */ 347 private function pageHeaderStartHandler() { 348 array_push($this->print_data_stack, $this->print_data); 349 $this->print_data = false; 350 array_push($this->wt_report_stack, $this->wt_report); 351 $this->wt_report = $this->report_root->createPageHeader(); 352 } 353 354 /** 355 * XML <pageHeaderEndHandler> 356 */ 357 private function pageHeaderEndHandler() { 358 $this->print_data = array_pop($this->print_data_stack); 359 $this->current_element = $this->wt_report; 360 $this->wt_report = array_pop($this->wt_report_stack); 361 $this->wt_report->addElement($this->current_element); 362 } 363 364 /** 365 * XML <bodyStartHandler> 366 */ 367 private function bodyStartHandler() { 368 $this->wt_report->setProcessing("B"); 369 } 370 371 /** 372 * XML <footerStartHandler> 373 */ 374 private function footerStartHandler() { 375 $this->wt_report->setProcessing("F"); 376 } 377 378 /** 379 * XML <Cell> 380 * 381 * @param array $attrs an array of key value pairs for the attributes 382 */ 383 private function cellStartHandler($attrs) { 384 // string The text alignment of the text in this box. 385 $align = ""; 386 if (!empty($attrs['align'])) { 387 $align = $attrs['align']; 388 // RTL supported left/right alignment 389 if ($align == "rightrtl") { 390 if ($this->wt_report->rtl) { 391 $align = "left"; 392 } else { 393 $align = "right"; 394 } 395 } elseif ($align == "leftrtl") { 396 if ($this->wt_report->rtl) { 397 $align = "right"; 398 } else { 399 $align = "left"; 400 } 401 } 402 } 403 404 // string The color to fill the background of this cell 405 $bgcolor = ""; 406 if (!empty($attrs['bgcolor'])) { 407 $bgcolor = $attrs['bgcolor']; 408 } 409 410 // int Whether or not the background should be painted 411 $fill = 1; 412 if (isset($attrs['fill'])) { 413 if ($attrs['fill'] === "0") { 414 $fill = 0; 415 } elseif ($attrs['fill'] === "1") { 416 $fill = 1; 417 } 418 } 419 420 $reseth = true; 421 // boolean if true reset the last cell height (default true) 422 if (isset($attrs['reseth'])) { 423 if ($attrs['reseth'] === "0") { 424 $reseth = false; 425 } elseif ($attrs['reseth'] === "1") { 426 $reseth = true; 427 } 428 } 429 430 // mixed Whether or not a border should be printed around this box 431 $border = 0; 432 if (!empty($attrs['border'])) { 433 $border = $attrs['border']; 434 } 435 // string Border color in HTML code 436 $bocolor = ""; 437 if (!empty($attrs['bocolor'])) { 438 $bocolor = $attrs['bocolor']; 439 } 440 441 // int Cell height (expressed in points) The starting height of this cell. If the text wraps the height will automatically be adjusted. 442 $height = 0; 443 if (!empty($attrs['height'])) { 444 $height = (int) $attrs['height']; 445 } 446 // int Cell width (expressed in points) Setting the width to 0 will make it the width from the current location to the right margin. 447 $width = 0; 448 if (!empty($attrs['width'])) { 449 $width = (int) $attrs['width']; 450 } 451 452 // int Stretch carachter mode 453 $stretch = 0; 454 if (!empty($attrs['stretch'])) { 455 $stretch = (int) $attrs['stretch']; 456 } 457 458 // mixed Position the left corner of this box on the page. The default is the current position. 459 $left = "."; 460 if (isset($attrs['left'])) { 461 if ($attrs['left'] === ".") { 462 $left = "."; 463 } elseif (!empty($attrs['left'])) { 464 $left = (int) $attrs['left']; 465 } elseif ($attrs['left'] === "0") { 466 $left = 0; 467 } 468 } 469 // mixed Position the top corner of this box on the page. the default is the current position 470 $top = "."; 471 if (isset($attrs['top'])) { 472 if ($attrs['top'] === ".") { 473 $top = "."; 474 } elseif (!empty($attrs['top'])) { 475 $top = (int) $attrs['top']; 476 } elseif ($attrs['top'] === "0") { 477 $top = 0; 478 } 479 } 480 481 // string The name of the Style that should be used to render the text. 482 $style = ""; 483 if (!empty($attrs['style'])) { 484 $style = $attrs['style']; 485 } 486 487 // string Text color in html code 488 $tcolor = ""; 489 if (!empty($attrs['tcolor'])) { 490 $tcolor = $attrs['tcolor']; 491 } 492 493 // int Indicates where the current position should go after the call. 494 $ln = 0; 495 if (isset($attrs['newline'])) { 496 if (!empty($attrs['newline'])) { 497 $ln = (int) $attrs['newline']; 498 } elseif ($attrs['newline'] === "0") { 499 $ln = 0; 500 } 501 } 502 503 if ($align == "left") { 504 $align = "L"; 505 } elseif ($align == "right") { 506 $align = "R"; 507 } elseif ($align == "center") { 508 $align = "C"; 509 } elseif ($align == "justify") { 510 $align = "J"; 511 } 512 513 array_push($this->print_data_stack, $this->print_data); 514 $this->print_data = true; 515 516 $this->current_element = $this->report_root->createCell( 517 $width, 518 $height, 519 $border, 520 $align, 521 $bgcolor, 522 $style, 523 $ln, 524 $top, 525 $left, 526 $fill, 527 $stretch, 528 $bocolor, 529 $tcolor, 530 $reseth 531 ); 532 } 533 534 /** 535 * XML </Cell> 536 */ 537 private function cellEndHandler() { 538 $this->print_data = array_pop($this->print_data_stack); 539 $this->wt_report->addElement($this->current_element); 540 } 541 542 /** 543 * XML <Now /> element handler 544 */ 545 private function nowStartHandler() { 546 $g = FunctionsDate::timestampToGedcomDate(WT_TIMESTAMP + WT_TIMESTAMP_OFFSET); 547 $this->current_element->addText($g->display()); 548 } 549 550 /** 551 * XML <PageNum /> element handler 552 */ 553 private function pageNumStartHandler() { 554 $this->current_element->addText("#PAGENUM#"); 555 } 556 557 /** 558 * XML <TotalPages /> element handler 559 */ 560 private function totalPagesStartHandler() { 561 $this->current_element->addText("{{:ptp:}}"); 562 } 563 564 /** 565 * Called at the start of an element. 566 * 567 * @param array $attrs an array of key value pairs for the attributes 568 */ 569 private function gedcomStartHandler($attrs) { 570 global $WT_TREE; 571 572 if ($this->process_gedcoms > 0) { 573 $this->process_gedcoms++; 574 575 return; 576 } 577 578 $tag = $attrs['id']; 579 $tag = str_replace("@fact", $this->fact, $tag); 580 $tags = explode(":", $tag); 581 $newgedrec = ''; 582 if (count($tags) < 2) { 583 $tmp = GedcomRecord::getInstance($attrs['id'], $WT_TREE); 584 $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($WT_TREE)) : ''; 585 } 586 if (empty($newgedrec)) { 587 $tgedrec = $this->gedrec; 588 $newgedrec = ''; 589 foreach ($tags as $tag) { 590 if (preg_match("/\\$(.+)/", $tag, $match)) { 591 if (isset($this->vars[$match[1]]['gedcom'])) { 592 $newgedrec = $this->vars[$match[1]]['gedcom']; 593 } else { 594 $tmp = GedcomRecord::getInstance($match[1], $WT_TREE); 595 $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($WT_TREE)) : ''; 596 } 597 } else { 598 if (preg_match("/@(.+)/", $tag, $match)) { 599 $gmatch = []; 600 if (preg_match("/\d $match[1] @([^@]+)@/", $tgedrec, $gmatch)) { 601 $tmp = GedcomRecord::getInstance($gmatch[1], $WT_TREE); 602 $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($WT_TREE)) : ''; 603 $tgedrec = $newgedrec; 604 } else { 605 $newgedrec = ''; 606 break; 607 } 608 } else { 609 $temp = explode(" ", trim($tgedrec)); 610 $level = $temp[0] + 1; 611 $newgedrec = Functions::getSubRecord($level, "$level $tag", $tgedrec); 612 $tgedrec = $newgedrec; 613 } 614 } 615 } 616 } 617 if (!empty($newgedrec)) { 618 array_push($this->gedrec_stack, [$this->gedrec, $this->fact, $this->desc]); 619 $this->gedrec = $newgedrec; 620 if (preg_match("/(\d+) (_?[A-Z0-9]+) (.*)/", $this->gedrec, $match)) { 621 $this->fact = $match[2]; 622 $this->desc = trim($match[3]); 623 } 624 } else { 625 $this->process_gedcoms++; 626 } 627 } 628 629 /** 630 * Called at the end of an element. 631 */ 632 private function gedcomEndHandler() { 633 if ($this->process_gedcoms > 0) { 634 $this->process_gedcoms--; 635 } else { 636 list($this->gedrec, $this->fact, $this->desc) = array_pop($this->gedrec_stack); 637 } 638 } 639 640 /** 641 * XML <textBoxStartHandler> 642 * 643 * @param array $attrs an array of key value pairs for the attributes 644 */ 645 private function textBoxStartHandler($attrs) { 646 // string Background color code 647 $bgcolor = ""; 648 if (!empty($attrs['bgcolor'])) { 649 $bgcolor = $attrs['bgcolor']; 650 } 651 652 // boolean Wether or not fill the background color 653 $fill = true; 654 if (isset($attrs['fill'])) { 655 if ($attrs['fill'] === "0") { 656 $fill = false; 657 } elseif ($attrs['fill'] === "1") { 658 $fill = true; 659 } 660 } 661 662 // var boolean Whether or not a border should be printed around this box. 0 = no border, 1 = border. Default is 0 663 $border = false; 664 if (isset($attrs['border'])) { 665 if ($attrs['border'] === "1") { 666 $border = true; 667 } elseif ($attrs['border'] === "0") { 668 $border = false; 669 } 670 } 671 672 // int The starting height of this cell. If the text wraps the height will automatically be adjusted 673 $height = 0; 674 if (!empty($attrs['height'])) { 675 $height = (int) $attrs['height']; 676 } 677 // int Setting the width to 0 will make it the width from the current location to the margin 678 $width = 0; 679 if (!empty($attrs['width'])) { 680 $width = (int) $attrs['width']; 681 } 682 683 // mixed Position the left corner of this box on the page. The default is the current position. 684 $left = "."; 685 if (isset($attrs['left'])) { 686 if ($attrs['left'] === ".") { 687 $left = "."; 688 } elseif (!empty($attrs['left'])) { 689 $left = (int) $attrs['left']; 690 } elseif ($attrs['left'] === "0") { 691 $left = 0; 692 } 693 } 694 // mixed Position the top corner of this box on the page. the default is the current position 695 $top = "."; 696 if (isset($attrs['top'])) { 697 if ($attrs['top'] === ".") { 698 $top = "."; 699 } elseif (!empty($attrs['top'])) { 700 $top = (int) $attrs['top']; 701 } elseif ($attrs['top'] === "0") { 702 $top = 0; 703 } 704 } 705 // 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 706 $newline = false; 707 if (isset($attrs['newline'])) { 708 if ($attrs['newline'] === "1") { 709 $newline = true; 710 } elseif ($attrs['newline'] === "0") { 711 $newline = false; 712 } 713 } 714 // boolean 715 $pagecheck = true; 716 if (isset($attrs['pagecheck'])) { 717 if ($attrs['pagecheck'] === "0") { 718 $pagecheck = false; 719 } elseif ($attrs['pagecheck'] === "1") { 720 $pagecheck = true; 721 } 722 } 723 // boolean Cell padding 724 $padding = true; 725 if (isset($attrs['padding'])) { 726 if ($attrs['padding'] === "0") { 727 $padding = false; 728 } elseif ($attrs['padding'] === "1") { 729 $padding = true; 730 } 731 } 732 // boolean Reset this box Height 733 $reseth = false; 734 if (isset($attrs['reseth'])) { 735 if ($attrs['reseth'] === "1") { 736 $reseth = true; 737 } elseif ($attrs['reseth'] === "0") { 738 $reseth = false; 739 } 740 } 741 742 // string Style of rendering 743 $style = ""; 744 745 array_push($this->print_data_stack, $this->print_data); 746 $this->print_data = false; 747 748 array_push($this->wt_report_stack, $this->wt_report); 749 $this->wt_report = $this->report_root->createTextBox( 750 $width, 751 $height, 752 $border, 753 $bgcolor, 754 $newline, 755 $left, 756 $top, 757 $pagecheck, 758 $style, 759 $fill, 760 $padding, 761 $reseth 762 ); 763 } 764 765 /** 766 * XML <textBoxEndHandler> 767 */ 768 private function textBoxEndHandler() { 769 $this->print_data = array_pop($this->print_data_stack); 770 $this->current_element = $this->wt_report; 771 $this->wt_report = array_pop($this->wt_report_stack); 772 $this->wt_report->addElement($this->current_element); 773 } 774 775 /** 776 * XLM <Text>. 777 * 778 * @param array $attrs an array of key value pairs for the attributes 779 */ 780 private function textStartHandler($attrs) { 781 array_push($this->print_data_stack, $this->print_data); 782 $this->print_data = true; 783 784 // string The name of the Style that should be used to render the text. 785 $style = ""; 786 if (!empty($attrs['style'])) { 787 $style = $attrs['style']; 788 } 789 790 // string The color of the text - Keep the black color as default 791 $color = ""; 792 if (!empty($attrs['color'])) { 793 $color = $attrs['color']; 794 } 795 796 $this->current_element = $this->report_root->createText($style, $color); 797 } 798 799 /** 800 * XML </Text> 801 */ 802 private function textEndHandler() { 803 $this->print_data = array_pop($this->print_data_stack); 804 $this->wt_report->addElement($this->current_element); 805 } 806 807 /** 808 * XML <GetPersonName/> 809 * 810 * Get the name 811 * 1. id is empty - current GEDCOM record 812 * 2. id is set with a record id 813 * 814 * @param array $attrs an array of key value pairs for the attributes 815 */ 816 private function getPersonNameStartHandler($attrs) { 817 global $WT_TREE; 818 819 $id = ""; 820 $match = []; 821 if (empty($attrs['id'])) { 822 if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) { 823 $id = $match[1]; 824 } 825 } else { 826 if (preg_match("/\\$(.+)/", $attrs['id'], $match)) { 827 if (isset($this->vars[$match[1]]['id'])) { 828 $id = $this->vars[$match[1]]['id']; 829 } 830 } else { 831 if (preg_match("/@(.+)/", $attrs['id'], $match)) { 832 $gmatch = []; 833 if (preg_match("/\d $match[1] @([^@]+)@/", $this->gedrec, $gmatch)) { 834 $id = $gmatch[1]; 835 } 836 } else { 837 $id = $attrs['id']; 838 } 839 } 840 } 841 if (!empty($id)) { 842 $record = GedcomRecord::getInstance($id, $WT_TREE); 843 if (is_null($record)) { 844 return; 845 } 846 if (!$record->canShowName()) { 847 $this->current_element->addText(I18N::translate('Private')); 848 } else { 849 $name = $record->getFullName(); 850 $name = preg_replace( 851 ['/<span class="starredname">/', '/<\/span><\/span>/', '/<\/span>/'], 852 ['«', '', '»'], 853 $name 854 ); 855 $name = strip_tags($name); 856 if (!empty($attrs['truncate'])) { 857 if (mb_strlen($name) > $attrs['truncate']) { 858 $name = preg_replace("/\(.*\) ?/", '', $name); //removes () and text inbetween - what about ", [ and { etc? 859 $words = preg_split('/[, -]+/', $name); // names separated with space, comma or hyphen - any others? 860 $name = $words[count($words) - 1]; 861 for ($i = count($words) - 2; $i >= 0; $i--) { 862 $len = mb_strlen($name); 863 for ($j = count($words) - 3; $j >= 0; $j--) { 864 $len += mb_strlen($words[$j]); 865 } 866 if ($len > $attrs['truncate']) { 867 $first_letter = mb_substr($words[$i], 0, 1); 868 // Do not show " of nick-names 869 if ($first_letter != "\"") { 870 $name = mb_substr($words[$i], 0, 1) . '. ' . $name; 871 } 872 } else { 873 $name = $words[$i] . ' ' . $name; 874 } 875 } 876 } 877 } else { 878 $addname = $record->getAddName(); 879 $addname = preg_replace( 880 ['/<span class="starredname">/', '/<\/span><\/span>/', '/<\/span>/'], 881 ['«', '', '»'], 882 $addname 883 ); 884 $addname = strip_tags($addname); 885 if (!empty($addname)) { 886 $name .= " " . $addname; 887 } 888 } 889 $this->current_element->addText(trim($name)); 890 } 891 } 892 } 893 894 /** 895 * XML <GedcomValue/> 896 * 897 * @param array $attrs an array of key value pairs for the attributes 898 */ 899 private function gedcomValueStartHandler($attrs) { 900 global $WT_TREE; 901 902 $id = ""; 903 $match = []; 904 if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) { 905 $id = $match[1]; 906 } 907 908 if (isset($attrs['newline']) && $attrs['newline'] == "1") { 909 $useBreak = "1"; 910 } else { 911 $useBreak = "0"; 912 } 913 914 $tag = $attrs['tag']; 915 if (!empty($tag)) { 916 if ($tag == "@desc") { 917 $value = $this->desc; 918 $value = trim($value); 919 $this->current_element->addText($value); 920 } 921 if ($tag == "@id") { 922 $this->current_element->addText($id); 923 } else { 924 $tag = str_replace("@fact", $this->fact, $tag); 925 if (empty($attrs['level'])) { 926 $temp = explode(" ", trim($this->gedrec)); 927 $level = $temp[0]; 928 if ($level == 0) { 929 $level++; 930 } 931 } else { 932 $level = $attrs['level']; 933 } 934 $tags = preg_split('/[: ]/', $tag); 935 $value = $this->getGedcomValue($tag, $level, $this->gedrec); 936 switch (end($tags)) { 937 case 'DATE': 938 $tmp = new Date($value); 939 $value = $tmp->display(); 940 break; 941 case 'PLAC': 942 $tmp = new Place($value, $WT_TREE); 943 $value = $tmp->getShortName(); 944 break; 945 } 946 if ($useBreak == "1") { 947 // Insert <br> when multiple dates exist. 948 // This works around a TCPDF bug that incorrectly wraps RTL dates on LTR pages 949 $value = str_replace('(', '<br>(', $value); 950 $value = str_replace('<span dir="ltr"><br>', '<br><span dir="ltr">', $value); 951 $value = str_replace('<span dir="rtl"><br>', '<br><span dir="rtl">', $value); 952 if (substr($value, 0, 6) == '<br>') { 953 $value = substr($value, 6); 954 } 955 } 956 $tmp = explode(':', $tag); 957 if (in_array(end($tmp), ['NOTE', 'TEXT'])) { 958 $value = Filter::formatText($value, $WT_TREE); // We'll strip HTML in addText() 959 } 960 $this->current_element->addText($value); 961 } 962 } 963 } 964 965 /** 966 * XML <RepeatTag> 967 * 968 * @param array $attrs an array of key value pairs for the attributes 969 */ 970 private function repeatTagStartHandler($attrs) { 971 global $WT_TREE; 972 973 $this->process_repeats++; 974 if ($this->process_repeats > 1) { 975 return; 976 } 977 978 array_push($this->repeats_stack, [$this->repeats, $this->repeat_bytes]); 979 $this->repeats = []; 980 $this->repeat_bytes = xml_get_current_line_number($this->parser); 981 982 $tag = ""; 983 if (isset($attrs['tag'])) { 984 $tag = $attrs['tag']; 985 } 986 if (!empty($tag)) { 987 if ($tag == "@desc") { 988 $value = $this->desc; 989 $value = trim($value); 990 $this->current_element->addText($value); 991 } else { 992 $tag = str_replace("@fact", $this->fact, $tag); 993 $tags = explode(":", $tag); 994 $temp = explode(" ", trim($this->gedrec)); 995 $level = $temp[0]; 996 if ($level == 0) { 997 $level++; 998 } 999 $subrec = $this->gedrec; 1000 $t = $tag; 1001 $count = count($tags); 1002 $i = 0; 1003 while ($i < $count) { 1004 $t = $tags[$i]; 1005 if (!empty($t)) { 1006 if ($i < ($count - 1)) { 1007 $subrec = Functions::getSubRecord($level, "$level $t", $subrec); 1008 if (empty($subrec)) { 1009 $level--; 1010 $subrec = Functions::getSubRecord($level, "@ $t", $this->gedrec); 1011 if (empty($subrec)) { 1012 return; 1013 } 1014 } 1015 } 1016 $level++; 1017 } 1018 $i++; 1019 } 1020 $level--; 1021 $count = preg_match_all("/$level $t(.*)/", $subrec, $match, PREG_SET_ORDER); 1022 $i = 0; 1023 while ($i < $count) { 1024 $i++; 1025 // Privacy check - is this a link, and are we allowed to view the linked object? 1026 $subrecord = Functions::getSubRecord($level, "$level $t", $subrec, $i); 1027 if (preg_match('/^\d ' . WT_REGEX_TAG . ' @(' . WT_REGEX_XREF . ')@/', $subrecord, $xref_match)) { 1028 $linked_object = GedcomRecord::getInstance($xref_match[1], $WT_TREE); 1029 if ($linked_object && !$linked_object->canShow()) { 1030 continue; 1031 } 1032 } 1033 $this->repeats[] = $subrecord; 1034 } 1035 } 1036 } 1037 } 1038 1039 /** 1040 * XML </ RepeatTag> 1041 */ 1042 private function repeatTagEndHandler() { 1043 global $report; 1044 1045 $this->process_repeats--; 1046 if ($this->process_repeats > 0) { 1047 return; 1048 } 1049 1050 // Check if there is anything to repeat 1051 if (count($this->repeats) > 0) { 1052 // No need to load them if not used... 1053 1054 $lineoffset = 0; 1055 foreach ($this->repeats_stack as $rep) { 1056 $lineoffset += $rep[1]; 1057 } 1058 //-- read the xml from the file 1059 $lines = file($report); 1060 while (strpos($lines[$lineoffset + $this->repeat_bytes], "<RepeatTag") === false) { 1061 $lineoffset--; 1062 } 1063 $lineoffset++; 1064 $reportxml = "<tempdoc>\n"; 1065 $line_nr = $lineoffset + $this->repeat_bytes; 1066 // RepeatTag Level counter 1067 $count = 1; 1068 while (0 < $count) { 1069 if (strstr($lines[$line_nr], "<RepeatTag") !== false) { 1070 $count++; 1071 } elseif (strstr($lines[$line_nr], "</RepeatTag") !== false) { 1072 $count--; 1073 } 1074 if (0 < $count) { 1075 $reportxml .= $lines[$line_nr]; 1076 } 1077 $line_nr++; 1078 } 1079 // No need to drag this 1080 unset($lines); 1081 $reportxml .= "</tempdoc>\n"; 1082 // Save original values 1083 array_push($this->parser_stack, $this->parser); 1084 $oldgedrec = $this->gedrec; 1085 foreach ($this->repeats as $gedrec) { 1086 $this->gedrec = $gedrec; 1087 $repeat_parser = xml_parser_create(); 1088 $this->parser = $repeat_parser; 1089 xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false); 1090 xml_set_element_handler($repeat_parser, [$this, 'startElement'], [$this, 'endElement']); 1091 xml_set_character_data_handler($repeat_parser, [$this, 'characterData']); 1092 if (!xml_parse($repeat_parser, $reportxml, true)) { 1093 throw new \DomainException(sprintf( 1094 'RepeatTagEHandler XML error: %s at line %d', 1095 xml_error_string(xml_get_error_code($repeat_parser)), 1096 xml_get_current_line_number($repeat_parser) 1097 )); 1098 } 1099 xml_parser_free($repeat_parser); 1100 } 1101 // Restore original values 1102 $this->gedrec = $oldgedrec; 1103 $this->parser = array_pop($this->parser_stack); 1104 } 1105 list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack); 1106 } 1107 1108 /** 1109 * Variable lookup 1110 * 1111 * Retrieve predefined variables : 1112 * 1113 * @ desc GEDCOM fact description, example: 1114 * 1 EVEN This is a description 1115 * @ fact GEDCOM fact tag, such as BIRT, DEAT etc. 1116 * $ I18N::translate('....') 1117 * $ language_settings[] 1118 * 1119 * @param array $attrs an array of key value pairs for the attributes 1120 */ 1121 private function varStartHandler($attrs) { 1122 if (empty($attrs['var'])) { 1123 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)); 1124 } 1125 1126 $var = $attrs['var']; 1127 // SetVar element preset variables 1128 if (!empty($this->vars[$var]['id'])) { 1129 $var = $this->vars[$var]['id']; 1130 } else { 1131 $tfact = $this->fact; 1132 if (($this->fact === "EVEN" || $this->fact === "FACT") && $this->type !== " ") { 1133 // Use : 1134 // n TYPE This text if string 1135 $tfact = $this->type; 1136 } 1137 $var = str_replace(["@fact", "@desc"], [GedcomTag::getLabel($tfact), $this->desc], $var); 1138 if (preg_match('/^I18N::number\((.+)\)$/', $var, $match)) { 1139 $var = I18N::number($match[1]); 1140 } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $var, $match)) { 1141 $var = I18N::translate($match[1]); 1142 } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $var, $match)) { 1143 $var = I18N::translateContext($match[1], $match[2]); 1144 } 1145 } 1146 // Check if variable is set as a date and reformat the date 1147 if (isset($attrs['date'])) { 1148 if ($attrs['date'] === "1") { 1149 $g = new Date($var); 1150 $var = $g->display(); 1151 } 1152 } 1153 $this->current_element->addText($var); 1154 $this->text = $var; // Used for title/descriptio 1155 } 1156 1157 /** 1158 * XML <Facts> 1159 * 1160 * @param array $attrs an array of key value pairs for the attributes 1161 */ 1162 private function factsStartHandler($attrs) { 1163 global $WT_TREE; 1164 1165 $this->process_repeats++; 1166 if ($this->process_repeats > 1) { 1167 return; 1168 } 1169 1170 array_push($this->repeats_stack, [$this->repeats, $this->repeat_bytes]); 1171 $this->repeats = []; 1172 $this->repeat_bytes = xml_get_current_line_number($this->parser); 1173 1174 $id = ""; 1175 $match = []; 1176 if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) { 1177 $id = $match[1]; 1178 } 1179 $tag = ""; 1180 if (isset($attrs['ignore'])) { 1181 $tag .= $attrs['ignore']; 1182 } 1183 if (preg_match("/\\$(.+)/", $tag, $match)) { 1184 $tag = $this->vars[$match[1]]['id']; 1185 } 1186 1187 $record = GedcomRecord::getInstance($id, $WT_TREE); 1188 if (empty($attrs['diff']) && !empty($id)) { 1189 $facts = $record->getFacts(); 1190 Functions::sortFacts($facts); 1191 $this->repeats = []; 1192 $nonfacts = explode(',', $tag); 1193 foreach ($facts as $event) { 1194 if (!in_array($event->getTag(), $nonfacts)) { 1195 $this->repeats[] = $event->getGedcom(); 1196 } 1197 } 1198 } else { 1199 foreach ($record->getFacts() as $fact) { 1200 if ($fact->isPendingAddition() && $fact->getTag() !== 'CHAN') { 1201 $this->repeats[] = $fact->getGedcom(); 1202 } 1203 } 1204 } 1205 } 1206 1207 /** 1208 * XML </Facts> 1209 */ 1210 private function factsEndHandler() { 1211 global $report; 1212 1213 $this->process_repeats--; 1214 if ($this->process_repeats > 0) { 1215 return; 1216 } 1217 1218 // Check if there is anything to repeat 1219 if (count($this->repeats) > 0) { 1220 1221 $line = xml_get_current_line_number($this->parser) - 1; 1222 $lineoffset = 0; 1223 foreach ($this->repeats_stack as $rep) { 1224 $lineoffset += $rep[1]; 1225 } 1226 1227 //-- read the xml from the file 1228 $lines = file($report); 1229 while ($lineoffset + $this->repeat_bytes > 0 && strpos($lines[$lineoffset + $this->repeat_bytes], '<Facts ') === false) { 1230 $lineoffset--; 1231 } 1232 $lineoffset++; 1233 $reportxml = "<tempdoc>\n"; 1234 $i = $line + $lineoffset; 1235 $line_nr = $this->repeat_bytes + $lineoffset; 1236 while ($line_nr < $i) { 1237 $reportxml .= $lines[$line_nr]; 1238 $line_nr++; 1239 } 1240 // No need to drag this 1241 unset($lines); 1242 $reportxml .= "</tempdoc>\n"; 1243 // Save original values 1244 array_push($this->parser_stack, $this->parser); 1245 $oldgedrec = $this->gedrec; 1246 $count = count($this->repeats); 1247 $i = 0; 1248 while ($i < $count) { 1249 $this->gedrec = $this->repeats[$i]; 1250 $this->fact = ''; 1251 $this->desc = ''; 1252 if (preg_match('/1 (\w+)(.*)/', $this->gedrec, $match)) { 1253 $this->fact = $match[1]; 1254 if ($this->fact === 'EVEN' || $this->fact === 'FACT') { 1255 $tmatch = []; 1256 if (preg_match('/2 TYPE (.+)/', $this->gedrec, $tmatch)) { 1257 $this->type = trim($tmatch[1]); 1258 } else { 1259 $this->type = ' '; 1260 } 1261 } 1262 $this->desc = trim($match[2]); 1263 $this->desc .= Functions::getCont(2, $this->gedrec); 1264 } 1265 $repeat_parser = xml_parser_create(); 1266 $this->parser = $repeat_parser; 1267 xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false); 1268 xml_set_element_handler($repeat_parser, [$this, 'startElement'], [$this, 'endElement']); 1269 xml_set_character_data_handler($repeat_parser, [$this, 'characterData']); 1270 if (!xml_parse($repeat_parser, $reportxml, true)) { 1271 throw new \DomainException(sprintf( 1272 'FactsEHandler XML error: %s at line %d', 1273 xml_error_string(xml_get_error_code($repeat_parser)), 1274 xml_get_current_line_number($repeat_parser) 1275 )); 1276 } 1277 xml_parser_free($repeat_parser); 1278 $i++; 1279 } 1280 // Restore original values 1281 $this->parser = array_pop($this->parser_stack); 1282 $this->gedrec = $oldgedrec; 1283 } 1284 list($this->repeats, $this->repeat_bytes) = array_pop($this->repeats_stack); 1285 } 1286 1287 /** 1288 * Setting upp or changing variables in the XML 1289 * The XML variable name and value is stored in $this->vars 1290 * 1291 * @param array $attrs an array of key value pairs for the attributes 1292 */ 1293 private function setVarStartHandler($attrs) { 1294 if (empty($attrs['name'])) { 1295 throw new \DomainException('REPORT ERROR var: The attribute "name" is missing or not set in the XML file'); 1296 } 1297 1298 $name = $attrs['name']; 1299 $value = $attrs['value']; 1300 $match = []; 1301 // Current GEDCOM record strings 1302 if ($value == "@ID") { 1303 if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) { 1304 $value = $match[1]; 1305 } 1306 } elseif ($value == "@fact") { 1307 $value = $this->fact; 1308 } elseif ($value == "@desc") { 1309 $value = $this->desc; 1310 } elseif ($value == "@generation") { 1311 $value = $this->generation; 1312 } elseif (preg_match("/@(\w+)/", $value, $match)) { 1313 $gmatch = []; 1314 if (preg_match("/\d $match[1] (.+)/", $this->gedrec, $gmatch)) { 1315 $value = str_replace("@", "", trim($gmatch[1])); 1316 } 1317 } 1318 if (preg_match("/\\$(\w+)/", $name, $match)) { 1319 $name = $this->vars["'" . $match[1] . "'"]['id']; 1320 } 1321 $count = preg_match_all("/\\$(\w+)/", $value, $match, PREG_SET_ORDER); 1322 $i = 0; 1323 while ($i < $count) { 1324 $t = $this->vars[$match[$i][1]]['id']; 1325 $value = preg_replace("/\\$" . $match[$i][1] . "/", $t, $value, 1); 1326 $i++; 1327 } 1328 if (preg_match('/^I18N::number\((.+)\)$/', $value, $match)) { 1329 $value = I18N::number($match[1]); 1330 } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $value, $match)) { 1331 $value = I18N::translate($match[1]); 1332 } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $value, $match)) { 1333 $value = I18N::translateContext($match[1], $match[2]); 1334 } 1335 // Arithmetic functions 1336 if (preg_match("/(\d+)\s*([\-\+\*\/])\s*(\d+)/", $value, $match)) { 1337 switch ($match[2]) { 1338 case "+": 1339 $t = $match[1] + $match[3]; 1340 $value = preg_replace("/" . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . "/", $t, $value); 1341 break; 1342 case "-": 1343 $t = $match[1] - $match[3]; 1344 $value = preg_replace("/" . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . "/", $t, $value); 1345 break; 1346 case "*": 1347 $t = $match[1] * $match[3]; 1348 $value = preg_replace("/" . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . "/", $t, $value); 1349 break; 1350 case "/": 1351 $t = $match[1] / $match[3]; 1352 $value = preg_replace("/" . $match[1] . "\s*([\-\+\*\/])\s*" . $match[3] . "/", $t, $value); 1353 break; 1354 } 1355 } 1356 if (strpos($value, "@") !== false) { 1357 $value = ""; 1358 } 1359 $this->vars[$name]['id'] = $value; 1360 } 1361 1362 /** 1363 * XML <if > start element 1364 * 1365 * @param array $attrs an array of key value pairs for the attributes 1366 */ 1367 private function ifStartHandler($attrs) { 1368 if ($this->process_ifs > 0) { 1369 $this->process_ifs++; 1370 1371 return; 1372 } 1373 1374 $condition = $attrs['condition']; 1375 $condition = $this->substituteVars($condition, true); 1376 $condition = str_replace([" LT ", " GT "], ["<", ">"], $condition); 1377 // Replace the first accurance only once of @fact:DATE or in any other combinations to the current fact, such as BIRT 1378 $condition = str_replace("@fact:", $this->fact . ':', $condition); 1379 $match = []; 1380 $count = preg_match_all("/@([\w:\.]+)/", $condition, $match, PREG_SET_ORDER); 1381 $i = 0; 1382 while ($i < $count) { 1383 $id = $match[$i][1]; 1384 $value = '""'; 1385 if ($id == "ID") { 1386 if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) { 1387 $value = "'" . $match[1] . "'"; 1388 } 1389 } elseif ($id === "fact") { 1390 $value = '"' . $this->fact . '"'; 1391 } elseif ($id === "desc") { 1392 $value = '"' . addslashes($this->desc) . '"'; 1393 } elseif ($id === "generation") { 1394 $value = '"' . $this->generation . '"'; 1395 } else { 1396 1397 $temp = explode(" ", trim($this->gedrec)); 1398 $level = $temp[0]; 1399 if ($level == 0) { 1400 $level++; 1401 } 1402 $value = $this->getGedcomValue($id, $level, $this->gedrec); 1403 if (empty($value)) { 1404 $level++; 1405 $value = $this->getGedcomValue($id, $level, $this->gedrec); 1406 } 1407 $value = preg_replace('/^@(' . WT_REGEX_XREF . ')@$/', '$1', $value); 1408 $value = '"' . addslashes($value) . '"'; 1409 } 1410 $condition = str_replace("@$id", $value, $condition); 1411 $i++; 1412 } 1413 $ret = eval("return (bool) ($condition);"); 1414 if (!$ret) { 1415 $this->process_ifs++; 1416 } 1417 } 1418 1419 /** 1420 * XML <if /> end element 1421 */ 1422 private function ifEndHandler() { 1423 if ($this->process_ifs > 0) { 1424 $this->process_ifs--; 1425 } 1426 } 1427 1428 /** 1429 * XML <Footnote > start element 1430 * Collect the Footnote links 1431 * GEDCOM Records that are protected by Privacy setting will be ignore 1432 * 1433 * @param array $attrs an array of key value pairs for the attributes 1434 */ 1435 private function footnoteStartHandler($attrs) { 1436 global $WT_TREE; 1437 1438 $id = ""; 1439 if (preg_match("/[0-9] (.+) @(.+)@/", $this->gedrec, $match)) { 1440 $id = $match[2]; 1441 } 1442 $record = GedcomRecord::getInstance($id, $WT_TREE); 1443 if ($record && $record->canShow()) { 1444 array_push($this->print_data_stack, $this->print_data); 1445 $this->print_data = true; 1446 $style = ""; 1447 if (!empty($attrs['style'])) { 1448 $style = $attrs['style']; 1449 } 1450 $this->footnote_element = $this->current_element; 1451 $this->current_element = $this->report_root->createFootnote($style); 1452 } else { 1453 $this->print_data = false; 1454 $this->process_footnote = false; 1455 } 1456 } 1457 1458 /** 1459 * XML <Footnote /> end element 1460 * Print the collected Footnote data 1461 */ 1462 private function footnoteEndHandler() { 1463 if ($this->process_footnote) { 1464 $this->print_data = array_pop($this->print_data_stack); 1465 $temp = trim($this->current_element->getValue()); 1466 if (strlen($temp) > 3) { 1467 $this->wt_report->addElement($this->current_element); 1468 } 1469 $this->current_element = $this->footnote_element; 1470 } else { 1471 $this->process_footnote = true; 1472 } 1473 } 1474 1475 /** 1476 * XML <FootnoteTexts /> element 1477 */ 1478 private function footnoteTextsStartHandler() { 1479 $temp = "footnotetexts"; 1480 $this->wt_report->addElement($temp); 1481 } 1482 1483 /** 1484 * XML <AgeAtDeath /> element handler 1485 */ 1486 private function ageAtDeathStartHandler() { 1487 // This duplicates functionality in FunctionsPrint::format_fact_date() 1488 global $factrec, $WT_TREE; 1489 1490 $match = []; 1491 if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) { 1492 $person = Individual::getInstance($match[1], $WT_TREE); 1493 // Recorded age 1494 if (preg_match('/\n2 AGE (.+)/', $factrec, $match)) { 1495 $fact_age = $match[1]; 1496 } else { 1497 $fact_age = ''; 1498 } 1499 if (preg_match('/\n2 HUSB\n3 AGE (.+)/', $factrec, $match)) { 1500 $husb_age = $match[1]; 1501 } else { 1502 $husb_age = ''; 1503 } 1504 if (preg_match('/\n2 WIFE\n3 AGE (.+)/', $factrec, $match)) { 1505 $wife_age = $match[1]; 1506 } else { 1507 $wife_age = ''; 1508 } 1509 1510 // Calculated age 1511 $birth_date = $person->getBirthDate(); 1512 // Can't use getDeathDate(), as this also gives BURI/CREM events, which 1513 // wouldn't give the correct "days after death" result for people with 1514 // no DEAT. 1515 $death_event = $person->getFirstFact('DEAT'); 1516 if ($death_event) { 1517 $death_date = $death_event->getDate(); 1518 } else { 1519 $death_date = new Date(''); 1520 } 1521 $value = ''; 1522 if (Date::compare($birth_date, $death_date) <= 0 || !$person->isDead()) { 1523 $age = Date::getAgeGedcom($birth_date, $death_date); 1524 // Only show calculated age if it differs from recorded age 1525 if ($age != '' && $age != "0d") { 1526 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 1527 ) { 1528 $value = FunctionsDate::getAgeAtEvent($age); 1529 $abbrev = substr($value, 0, strpos($value, ' ') + 5); 1530 if ($value !== $abbrev) { 1531 $value = $abbrev . '.'; 1532 } 1533 } 1534 } 1535 } 1536 $this->current_element->addText($value); 1537 } 1538 } 1539 1540 /** 1541 * XML element Forced line break handler - HTML code 1542 */ 1543 private function brStartHandler() { 1544 if ($this->print_data && $this->process_gedcoms === 0) { 1545 $this->current_element->addText('<br>'); 1546 } 1547 } 1548 1549 /** 1550 * XML <sp />element Forced space handler 1551 */ 1552 private function spStartHandler() { 1553 if ($this->print_data && $this->process_gedcoms === 0) { 1554 $this->current_element->addText(' '); 1555 } 1556 } 1557 1558 /** 1559 * XML <HighlightedImage/> 1560 * 1561 * @param array $attrs an array of key value pairs for the attributes 1562 */ 1563 private function highlightedImageStartHandler($attrs) { 1564 global $WT_TREE; 1565 1566 $id = ''; 1567 $match = []; 1568 if (preg_match("/0 @(.+)@/", $this->gedrec, $match)) { 1569 $id = $match[1]; 1570 } 1571 1572 // mixed Position the top corner of this box on the page. the default is the current position 1573 $top = '.'; 1574 if (isset($attrs['top'])) { 1575 if ($attrs['top'] === '0') { 1576 $top = 0; 1577 } elseif ($attrs['top'] === '.') { 1578 $top = '.'; 1579 } elseif (!empty($attrs['top'])) { 1580 $top = (int) $attrs['top']; 1581 } 1582 } 1583 1584 // mixed Position the left corner of this box on the page. the default is the current position 1585 $left = '.'; 1586 if (isset($attrs['left'])) { 1587 if ($attrs['left'] === '0') { 1588 $left = 0; 1589 } elseif ($attrs['left'] === '.') { 1590 $left = '.'; 1591 } elseif (!empty($attrs['left'])) { 1592 $left = (int) $attrs['left']; 1593 } 1594 } 1595 1596 // string Align the image in left, center, right 1597 $align = ''; 1598 if (!empty($attrs['align'])) { 1599 $align = $attrs['align']; 1600 } 1601 1602 // string Next Line should be T:next to the image, N:next line 1603 $ln = ''; 1604 if (!empty($attrs['ln'])) { 1605 $ln = $attrs['ln']; 1606 } 1607 1608 $width = 0; 1609 $height = 0; 1610 if (!empty($attrs['width'])) { 1611 $width = (int) $attrs['width']; 1612 } 1613 if (!empty($attrs['height'])) { 1614 $height = (int) $attrs['height']; 1615 } 1616 1617 $person = Individual::getInstance($id, $WT_TREE); 1618 $mediaobject = $person->findHighlightedMedia(); 1619 if ($mediaobject) { 1620 $attributes = $mediaobject->getImageAttributes('thumb'); 1621 if (in_array( 1622 $attributes['ext'], 1623 [ 1624 'GIF', 1625 'JPG', 1626 'PNG', 1627 'SWF', 1628 'PSD', 1629 'BMP', 1630 'TIFF', 1631 'TIFF', 1632 'JPC', 1633 'JP2', 1634 'JPX', 1635 'JB2', 1636 'SWC', 1637 'IFF', 1638 'WBMP', 1639 'XBM', 1640 ] 1641 ) && $mediaobject->canShow() && $mediaobject->fileExists('thumb') 1642 ) { 1643 if ($width > 0 && $height == 0) { 1644 $perc = $width / $attributes['adjW']; 1645 $height = round($attributes['adjH'] * $perc); 1646 } elseif ($height > 0 && $width == 0) { 1647 $perc = $height / $attributes['adjH']; 1648 $width = round($attributes['adjW'] * $perc); 1649 } else { 1650 $width = $attributes['adjW']; 1651 $height = $attributes['adjH']; 1652 } 1653 $image = $this->report_root->createImageFromObject($mediaobject, $left, $top, $width, $height, $align, $ln); 1654 $this->wt_report->addElement($image); 1655 } 1656 } 1657 } 1658 1659 /** 1660 * XML <Image/> 1661 * 1662 * @param array $attrs an array of key value pairs for the attributes 1663 */ 1664 private function imageStartHandler($attrs) { 1665 global $WT_TREE; 1666 1667 // mixed Position the top corner of this box on the page. the default is the current position 1668 $top = '.'; 1669 if (isset($attrs['top'])) { 1670 if ($attrs['top'] === "0") { 1671 $top = 0; 1672 } elseif ($attrs['top'] === '.') { 1673 $top = '.'; 1674 } elseif (!empty($attrs['top'])) { 1675 $top = (int) $attrs['top']; 1676 } 1677 } 1678 1679 // mixed Position the left corner of this box on the page. the default is the current position 1680 $left = '.'; 1681 if (isset($attrs['left'])) { 1682 if ($attrs['left'] === '0') { 1683 $left = 0; 1684 } elseif ($attrs['left'] === '.') { 1685 $left = '.'; 1686 } elseif (!empty($attrs['left'])) { 1687 $left = (int) $attrs['left']; 1688 } 1689 } 1690 1691 // string Align the image in left, center, right 1692 $align = ''; 1693 if (!empty($attrs['align'])) { 1694 $align = $attrs['align']; 1695 } 1696 1697 // string Next Line should be T:next to the image, N:next line 1698 $ln = 'T'; 1699 if (!empty($attrs['ln'])) { 1700 $ln = $attrs['ln']; 1701 } 1702 1703 $width = 0; 1704 $height = 0; 1705 if (!empty($attrs['width'])) { 1706 $width = (int) $attrs['width']; 1707 } 1708 if (!empty($attrs['height'])) { 1709 $height = (int) $attrs['height']; 1710 } 1711 1712 $file = ''; 1713 if (!empty($attrs['file'])) { 1714 $file = $attrs['file']; 1715 } 1716 if ($file == "@FILE") { 1717 $match = []; 1718 if (preg_match("/\d OBJE @(.+)@/", $this->gedrec, $match)) { 1719 $mediaobject = Media::getInstance($match[1], $WT_TREE); 1720 $attributes = $mediaobject->getImageAttributes('thumb'); 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('thumb') 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 $that = $this; // PHP5.3 cannot access $this inside a closure 2815 return preg_replace_callback( 2816 '/\$(\w+)/', 2817 function ($matches) use ($that, $quote) { 2818 if (isset($that->vars[$matches[1]]['id'])) { 2819 if ($quote) { 2820 return "'" . addcslashes($that->vars[$matches[1]]['id'], "'") . "'"; 2821 } else { 2822 return $that->vars[$matches[1]]['id']; 2823 } 2824 } else { 2825 Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1])); 2826 2827 return '$' . $matches[1]; 2828 } 2829 }, 2830 $expression 2831 ); 2832 } 2833} 2834