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