Passed
Pull Request — main (#5055)
by
unknown
07:23
created

ReportParserGenerate::getPersonNameStartHandler()   F

Complexity

Conditions 26
Paths 1512

Size

Total Lines 82
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 26
eloc 62
nc 1512
nop 1
dl 0
loc 82
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2023 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 <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Report;
21
22
use DomainException;
23
use Fisharebest\Webtrees\Auth;
24
use Fisharebest\Webtrees\Date;
25
//use Fisharebest\Webtrees\DB; // webtrees 2.2.x
26
use Fisharebest\Webtrees\Elements\UnknownElement;
27
use Fisharebest\Webtrees\Factories\MarkdownFactory;
28
use Fisharebest\Webtrees\Family;
29
use Fisharebest\Webtrees\Gedcom;
30
use Fisharebest\Webtrees\GedcomRecord;
31
use Fisharebest\Webtrees\I18N;
32
use Fisharebest\Webtrees\Individual;
33
use Fisharebest\Webtrees\Log;
34
use Fisharebest\Webtrees\MediaFile;
35
use Fisharebest\Webtrees\Note;
36
use Fisharebest\Webtrees\Place;
37
use Fisharebest\Webtrees\Registry;
38
use Fisharebest\Webtrees\Tree;
39
use Illuminate\Database\Capsule\Manager as DB; // webtrees 2.1.x
40
use Illuminate\Database\Query\Builder;
41
use Illuminate\Database\Query\Expression;
42
use Illuminate\Database\Query\JoinClause;
43
use Illuminate\Support\Str;
44
use LogicException;
45
use Symfony\Component\Cache\Adapter\NullAdapter;
46
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
47
use XMLParser;
48
49
use function addcslashes;
50
use function addslashes;
51
use function array_pop;
52
use function array_shift;
53
use function assert;
54
use function count;
55
use function end;
56
use function explode;
57
use function file;
58
use function file_exists;
59
use function getimagesize;
60
use function imagecreatefromstring;
61
use function imagesx;
62
use function imagesy;
63
use function in_array;
64
use function ltrim;
65
use function method_exists;
66
use function preg_match;
67
use function preg_match_all;
68
use function preg_replace;
69
use function preg_replace_callback;
70
use function preg_split;
71
use function reset;
72
use function round;
73
use function sprintf;
74
use function str_contains;
75
use function str_ends_with;
76
use function str_replace;
77
use function str_starts_with;
78
use function strip_tags;
79
use function strlen;
80
use function strpos;
81
use function strtoupper;
82
use function substr;
83
use function substr_replace;
84
use function trim;
85
use function uasort;
86
use function xml_error_string;
87
use function xml_get_current_line_number;
88
use function xml_get_error_code;
89
use function xml_parse;
90
use function xml_parser_create;
91
use function xml_parser_free;
92
use function xml_parser_set_option;
93
use function xml_set_character_data_handler;
94
use function xml_set_element_handler;
95
96
use const PREG_OFFSET_CAPTURE;
97
use const PREG_SET_ORDER;
98
use const XML_OPTION_CASE_FOLDING;
99
100
/**
101
 * Class ReportParserGenerate - parse a report.xml file and generate the report.
102
 */
103
class ReportParserGenerate extends ReportParserBase
104
{
105
    /** Are we collecting data from <Footnote> elements */
106
    private bool $process_footnote = true;
107
108
    /** Are we currently outputting data? */
109
    private bool $print_data = false;
110
111
    /** @var array<int,bool> Push-down stack of $print_data */
112
    private array $print_data_stack = [];
113
114
    /** Are we processing GEDCOM data */
115
    private int $process_gedcoms = 0;
116
117
    /** Are we processing conditionals */
118
    private int $process_ifs = 0;
119
120
    /** Are we processing repeats */
121
    private int $process_repeats = 0;
122
123
    /** Quantity of data to repeat during loops */
124
    private int $repeat_bytes = 0;
125
126
    /** @var array<string> Repeated data when iterating over loops */
127
    private array $repeats = [];
128
129
    /** @var array<int,array<int,array<string>|int>> Nested repeating data */
130
    private array $repeats_stack = [];
131
132
    /** @var array<AbstractRenderer> Nested repeating data */
133
    private array $wt_report_stack = [];
134
135
    // Nested repeating data
136
    private XMLParser $parser;
137
138
    /** @var XMLParser[] (resource[] before PHP 8.0) Nested repeating data */
139
    private array $parser_stack = [];
140
141
    /** The current GEDCOM record */
142
    private string $gedrec = '';
143
144
    /** @var array<int,array<int,string>> Nested GEDCOM records */
145
    private array $gedrec_stack = [];
146
147
    /** @var ReportBaseElement The currently processed element */
148
    private $current_element;
149
150
    /** @var ReportBaseElement The currently processed element */
151
    private $footnote_element;
152
153
    /** The GEDCOM fact currently being processed */
154
    private string $fact = '';
155
156
    /** The GEDCOM value currently being processed */
157
    private string $desc = '';
158
159
    /** The GEDCOM type currently being processed */
160
    private string $type = '';
161
162
    /** The current generational level */
163
    private int $generation = 1;
164
165
    /** @var array<static|GedcomRecord> Source data for processing lists */
166
    private array $list = [];
167
168
    /** Number of items in lists */
169
    private int $list_total = 0;
170
171
    /** Number of items filtered from lists */
172
    private int $list_private = 0;
173
174
    /** @var string The filename of the XML report */
175
    protected $report;
176
177
    /** @var AbstractRenderer A factory for creating report elements */
178
    private $report_root;
179
180
    /** @var AbstractRenderer Nested report elements */
181
    private $wt_report;
182
183
    /** @var array<array<string>> Variables defined in the report at run-time */
184
    private array $vars;
185
186
    /** @var array<string> Family relationship */
187
    private array $mfrelation = [];
188
189
    private Tree $tree;
190
191
    // position of the TextBox: true=absolute or false=relative:
192
    private bool $abs_position = true;
0 ignored issues
show
introduced by
The private property $abs_position is not used, and could be removed.
Loading history...
193
194
    // Value of top positionof the TextBox
195
    private float $top_position = 0.0;
0 ignored issues
show
introduced by
The private property $top_position is not used, and could be removed.
Loading history...
196
197
    // Spec position for html textbox
198
    private bool $html_only_position = true;
0 ignored issues
show
introduced by
The private property $html_only_position is not used, and could be removed.
Loading history...
199
200
    /**
201
     * Create a parser for a report
202
     *
203
     * @param string               $report The XML filename
204
     * @param AbstractRenderer     $report_root
205
     * @param array<array<string>> $vars
206
     * @param Tree                 $tree
207
     */
208
    public function __construct(string $report, AbstractRenderer $report_root, array $vars, Tree $tree)
209
    {
210
        $this->report          = $report;
211
        $this->report_root     = $report_root;
212
        $this->wt_report       = $report_root;
213
        $this->current_element = new ReportBaseElement();
214
        $this->vars            = $vars;
215
        $this->tree            = $tree;
216
217
        parent::__construct($report);
218
    }
219
220
    /**
221
     * get a gedcom subrecord
222
     *
223
     * searches a gedcom record and returns a subrecord of it. A subrecord is defined starting at a
224
     * line with level N and all subsequent lines greater than N until the next N level is reached.
225
     * For example, the following is a BIRT subrecord:
226
     * <code>1 BIRT
227
     * 2 DATE 1 JAN 1900
228
     * 2 PLAC Phoenix, Maricopa, Arizona</code>
229
     * The following example is the DATE subrecord of the above BIRT subrecord:
230
     * <code>2 DATE 1 JAN 1900</code>
231
     *
232
     * @param int    $level   the N level of the subrecord to get
233
     * @param string $tag     a gedcom tag or string to search for in the record (ie 1 BIRT or 2 DATE)
234
     * @param string $gedrec  the parent gedcom record to search in
235
     * @param int    $num     this allows you to specify which matching <var>$tag</var> to get. Oftentimes a
236
     *                        gedcom record will have more that 1 of the same type of subrecord. An individual may have
237
     *                        multiple events for example. Passing $num=1 would get the first 1. Passing $num=2 would get the
238
     *                        second one, etc.
239
     *
240
     * @return string the subrecord that was found or an empty string "" if not found.
241
     */
242
    public static function getSubRecord(int $level, string $tag, string $gedrec, int $num = 1): string
243
    {
244
        if ($gedrec === '') {
245
            return '';
246
        }
247
        // -- adding \n before and after gedrec
248
        $gedrec       = "\n" . $gedrec . "\n";
249
        $tag          = trim($tag);
250
        $searchTarget = "~[\n]" . $tag . "[\s]~";
251
        $ct           = preg_match_all($searchTarget, $gedrec, $match, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
252
        if ($ct === 0) {
253
            return '';
254
        }
255
        if ($ct < $num) {
256
            return '';
257
        }
258
        $pos1 = $match[$num - 1][0][1];
259
        $pos2 = strpos($gedrec, "\n$level", $pos1 + 1);
260
        if (!$pos2) {
261
            $pos2 = strpos($gedrec, "\n1", $pos1 + 1);
262
        }
263
        if (!$pos2) {
264
            $pos2 = strpos($gedrec, "\nWT_", $pos1 + 1); // WT_SPOUSE, WT_FAMILY_ID ...
265
        }
266
        if (!$pos2) {
267
            return ltrim(substr($gedrec, $pos1));
268
        }
269
        $subrec = substr($gedrec, $pos1, $pos2 - $pos1);
270
271
        return ltrim($subrec);
272
    }
273
274
    /**
275
     * get CONT lines
276
     *
277
     * get the N+1 CONT or CONC lines of a gedcom subrecord
278
     *
279
     * @param int    $nlevel the level of the CONT lines to get
280
     * @param string $nrec   the gedcom subrecord to search in
281
     *
282
     * @return string a string with all CONT lines merged
283
     */
284
    public static function getCont(int $nlevel, string $nrec): string
285
    {
286
        $text = '';
287
288
        $subrecords = explode("\n", $nrec);
289
        foreach ($subrecords as $thisSubrecord) {
290
            if (substr($thisSubrecord, 0, 2) !== $nlevel . ' ') {
291
                continue;
292
            }
293
            $subrecordType = substr($thisSubrecord, 2, 4);
294
            if ($subrecordType === 'CONT') {
295
                $text .= "\n" . substr($thisSubrecord, 7);
296
            }
297
        }
298
299
        return $text;
300
    }
301
302
    /**
303
     * XML start element handler
304
     * This function is called whenever a starting element is reached
305
     * The element handler will be called if found, otherwise it must be HTML
306
     *
307
     * @param resource      $parser the resource handler for the XML parser
308
     * @param string        $name   the name of the XML element parsed
309
     * @param array<string> $attrs  an array of key value pairs for the attributes
310
     *
311
     * @return void
312
     */
313
    protected function startElement($parser, string $name, array $attrs): void
314
    {
315
        $newattrs = [];
316
317
        foreach ($attrs as $key => $value) {
318
            if (preg_match("/^\\$(\w+)$/", $value, $match)) {
319
                if (isset($this->vars[$match[1]]['id']) && !isset($this->vars[$match[1]]['gedcom'])) {
320
                    $value = $this->vars[$match[1]]['id'];
321
                }
322
            }
323
            $newattrs[$key] = $value;
324
        }
325
        $attrs = $newattrs;
326
        if ($this->process_footnote && ($this->process_ifs === 0 || $name === 'if') && ($this->process_gedcoms === 0 || $name === 'Gedcom') && ($this->process_repeats === 0 || $name === 'Facts' || $name === 'RepeatTag')) {
327
            $method = $name . 'StartHandler';
328
329
            if (method_exists($this, $method)) {
330
                $this->{$method}($attrs);
331
            }
332
        }
333
    }
334
335
    /**
336
     * XML end element handler
337
     * This function is called whenever an ending element is reached
338
     * The element handler will be called if found, otherwise it must be HTML
339
     *
340
     * @param resource $parser the resource handler for the XML parser
341
     * @param string   $name   the name of the XML element parsed
342
     *
343
     * @return void
344
     */
345
    protected function endElement($parser, string $name): void
346
    {
347
        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')) {
348
            $method = $name . 'EndHandler';
349
350
            if (method_exists($this, $method)) {
351
                $this->{$method}();
352
            }
353
        }
354
    }
355
356
    /**
357
     * XML character data handler
358
     *
359
     * @param resource $parser the resource handler for the XML parser
360
     * @param string   $data   the name of the XML element parsed
361
     *
362
     * @return void
363
     */
364
    protected function characterData($parser, string $data): void
365
    {
366
        if ($this->print_data && $this->process_gedcoms === 0 && $this->process_ifs === 0 && $this->process_repeats === 0) {
367
            $this->current_element->addText($data);
368
        }
369
    }
370
371
    /**
372
     * Handle <style>
373
     *
374
     * @param array<string> $attrs
375
     *
376
     * @return void
377
     */
378
    protected function styleStartHandler(array $attrs): void
379
    {
380
        if (empty($attrs['name'])) {
381
            throw new DomainException('REPORT ERROR Style: The "name" of the style is missing or not set in the XML file.');
382
        }
383
384
        $style = [
385
            'name'  => $attrs['name'],
386
            'font'  => $attrs['font'] ?? $this->wt_report->default_font,
387
            'size'  => (float) ($attrs['size'] ?? $this->wt_report->default_font_size),
388
            'style' => $attrs['style'] ?? '',
389
        ];
390
391
        $this->wt_report->addStyle($style);
392
    }
393
394
    /**
395
     * Handle <doc>
396
     * Sets up the basics of the document proparties
397
     *
398
     * @param array<string> $attrs
399
     *
400
     * @return void
401
     */
402
    protected function docStartHandler(array $attrs): void
403
    {
404
        $this->parser = $this->xml_parser;
405
406
        // Custom page width
407
        if (!empty($attrs['customwidth'])) {
408
            $this->wt_report->page_width = (float) $attrs['customwidth'];
409
        }
410
        // Custom Page height
411
        if (!empty($attrs['customheight'])) {
412
            $this->wt_report->page_height = (float) $attrs['customheight'];
413
        }
414
415
        // Left Margin
416
        if (isset($attrs['leftmargin'])) {
417
            if ($attrs['leftmargin'] === '0') {
418
                $this->wt_report->left_margin = 0;
419
            } elseif (!empty($attrs['leftmargin'])) {
420
                $this->wt_report->left_margin = (float) $attrs['leftmargin'];
421
            }
422
        }
423
        // Right Margin
424
        if (isset($attrs['rightmargin'])) {
425
            if ($attrs['rightmargin'] === '0') {
426
                $this->wt_report->right_margin = 0;
427
            } elseif (!empty($attrs['rightmargin'])) {
428
                $this->wt_report->right_margin = (float) $attrs['rightmargin'];
429
            }
430
        }
431
        // Top Margin
432
        if (isset($attrs['topmargin'])) {
433
            if ($attrs['topmargin'] === '0') {
434
                $this->wt_report->top_margin = 0;
435
            } elseif (!empty($attrs['topmargin'])) {
436
                $this->wt_report->top_margin = (float) $attrs['topmargin'];
437
            }
438
        }
439
        // Bottom Margin
440
        if (isset($attrs['bottommargin'])) {
441
            if ($attrs['bottommargin'] === '0') {
442
                $this->wt_report->bottom_margin = 0;
443
            } elseif (!empty($attrs['bottommargin'])) {
444
                $this->wt_report->bottom_margin = (float) $attrs['bottommargin'];
445
            }
446
        }
447
        // Header Margin
448
        if (isset($attrs['headermargin'])) {
449
            if ($attrs['headermargin'] === '0') {
450
                $this->wt_report->header_margin = 0;
451
            } elseif (!empty($attrs['headermargin'])) {
452
                $this->wt_report->header_margin = (float) $attrs['headermargin'];
453
            }
454
        }
455
        // Footer Margin
456
        if (isset($attrs['footermargin'])) {
457
            if ($attrs['footermargin'] === '0') {
458
                $this->wt_report->footer_margin = 0;
459
            } elseif (!empty($attrs['footermargin'])) {
460
                $this->wt_report->footer_margin = (float) $attrs['footermargin'];
461
            }
462
        }
463
464
        // Page Orientation
465
        if (!empty($attrs['orientation'])) {
466
            if ($attrs['orientation'] === 'landscape') {
467
                $this->wt_report->orientation = 'landscape';
468
            } elseif ($attrs['orientation'] === 'portrait') {
469
                $this->wt_report->orientation = 'portrait';
470
            }
471
        }
472
        // Page Size
473
        if (!empty($attrs['pageSize'])) {
474
            $this->wt_report->page_format = $attrs['pageSize'];
475
        }
476
477
        // Show Generated By...
478
        if (isset($attrs['showGeneratedBy'])) {
479
            if ($attrs['showGeneratedBy'] === '0') {
480
                $this->wt_report->show_generated_by = false;
481
            } elseif ($attrs['showGeneratedBy'] === '1') {
482
                $this->wt_report->show_generated_by = true;
483
            }
484
        }
485
486
        $this->wt_report->setup();
487
    }
488
489
    /**
490
     * Handle </doc>
491
     *
492
     * @return void
493
     */
494
    protected function docEndHandler(): void
495
    {
496
        $this->wt_report->run();
497
    }
498
499
    /**
500
     * Handle <header>
501
     *
502
     * @return void
503
     */
504
    protected function headerStartHandler(): void
505
    {
506
        // Clear the Header before any new elements are added
507
        $this->wt_report->clearHeader();
508
        $this->wt_report->setProcessing('H');
509
    }
510
511
    /**
512
     * Handle <body>
513
     *
514
     * @return void
515
     */
516
    protected function bodyStartHandler(): void
517
    {
518
        $this->wt_report->setProcessing('B');
519
    }
520
521
    /**
522
     * Handle <footer>
523
     *
524
     * @return void
525
     */
526
    protected function footerStartHandler(): void
527
    {
528
        $this->wt_report->setProcessing('F');
529
    }
530
531
    /**
532
     * Handle <cell>
533
     *
534
     * @param array<string,string> $attrs
535
     *
536
     * @return void
537
     */
538
    protected function cellStartHandler(array $attrs): void
539
    {
540
        // string The text alignment of the text in this box.
541
        $align = $attrs['align'] ?? '';
542
        // RTL supported left/right alignment
543
        if ($align === 'rightrtl') {
544
            if ($this->wt_report->rtl) {
545
                $align = 'left';
546
            } else {
547
                $align = 'right';
548
            }
549
        } elseif ($align === 'leftrtl') {
550
            if ($this->wt_report->rtl) {
551
                $align = 'right';
552
            } else {
553
                $align = 'left';
554
            }
555
        }
556
557
        // The color to fill the background of this cell
558
        $bgcolor = $attrs['bgcolor'] ?? '';
559
560
        // Whether the background should be painted
561
        $fill = (bool) ($attrs['fill'] ?? '0');
562
563
        // If true reset the last cell height
564
        $reseth = (bool) ($attrs['reseth'] ?? '1');
565
566
        // Whether a border should be printed around this box
567
        $border = $attrs['border'] ?? '';
568
569
        // string Border color in HTML code
570
        $bocolor = $attrs['bocolor'] ?? '';
571
572
        // Cell height (expressed in points) The starting height of this cell. If the text wraps the height will automatically be adjusted.
573
        $height = (int) ($attrs['height'] ?? '0');
574
575
        // int Cell width (expressed in points) Setting the width to 0 will make it the width from the current location to the right margin.
576
        $width = (int) ($attrs['width'] ?? '0');
577
578
        // Stretch character mode
579
        $stretch = (int) ($attrs['stretch'] ?? '0');
580
581
        // mixed Position the left corner of this box on the page. The default is the current position.
582
        $left = ReportBaseElement::CURRENT_POSITION;
583
        if (isset($attrs['left'])) {
584
            if ($attrs['left'] === '.') {
585
                $left = ReportBaseElement::CURRENT_POSITION;
586
            } elseif (!empty($attrs['left'])) {
587
                $left = (float) $attrs['left'];
588
            } elseif ($attrs['left'] === '0') {
589
                $left = 0.0;
590
            }
591
        }
592
        // mixed Position the top corner of this box on the page. the default is the current position
593
        $top = ReportBaseElement::CURRENT_POSITION;
594
        if (isset($attrs['top'])) {
595
            if ($attrs['top'] === '.') {
596
                $top = ReportBaseElement::CURRENT_POSITION;
597
            } elseif (!empty($attrs['top'])) {
598
                $top = (float) $attrs['top'];
599
            } elseif ($attrs['top'] === '0') {
600
                $top = 0.0;
601
            }
602
        }
603
604
        // The name of the Style that should be used to render the text.
605
        $style = $attrs['style'] ?? '';
606
607
        // string Text color in html code
608
        $tcolor = $attrs['tcolor'] ?? '';
609
610
        // int Indicates where the current position should go after the call.
611
        $ln = 0;
612
        if (isset($attrs['newline'])) {
613
            if (!empty($attrs['newline'])) {
614
                $ln = (int) $attrs['newline'];
615
            } elseif ($attrs['newline'] === '0') {
616
                $ln = 0;
617
            }
618
        }
619
620
        if ($align === 'left') {
621
            $align = 'L';
622
        } elseif ($align === 'right') {
623
            $align = 'R';
624
        } elseif ($align === 'center') {
625
            $align = 'C';
626
        } elseif ($align === 'justify') {
627
            $align = 'J';
628
        }
629
630
        $this->print_data_stack[] = $this->print_data;
631
        $this->print_data         = true;
632
633
        $this->current_element = $this->report_root->createCell(
634
            (int) $width,
635
            (int) $height,
636
            $border,
637
            $align,
638
            $bgcolor,
639
            $style,
640
            $ln,
641
            $top,
642
            $left,
643
            $fill,
644
            $stretch,
645
            $bocolor,
646
            $tcolor,
647
            $reseth
648
        );
649
650
        // set string URL to be a link
651
        if (isset($attrs['url'])) {
652
            $url = $attrs['url'];
653
            $this->current_element->setUrl($url);
654
        } else {
655
            $url = "";
0 ignored issues
show
Unused Code introduced by
The assignment to $url is dead and can be removed.
Loading history...
656
        }
657
    }
658
659
    /**
660
     * Handle </cell>
661
     *
662
     * @return void
663
     */
664
    protected function cellEndHandler(): void
665
    {
666
        $this->print_data = array_pop($this->print_data_stack);
667
        $this->wt_report->addElement($this->current_element);
668
    }
669
670
    /**
671
     * Handle <now />
672
     *
673
     * @return void
674
     */
675
    protected function nowStartHandler(): void
676
    {
677
        $this->current_element->addText(Registry::timestampFactory()->now()->isoFormat('LLLL'));
678
    }
679
680
    /**
681
     * Handle <pageNum />
682
     *
683
     * @return void
684
     */
685
    protected function pageNumStartHandler(): void
686
    {
687
        $this->current_element->addText('#PAGENUM#');
688
    }
689
690
    /**
691
     * Handle <totalPages />
692
     *
693
     * @return void
694
     */
695
    protected function totalPagesStartHandler(): void
696
    {
697
        $this->current_element->addText('{{:ptp:}}');
698
    }
699
700
    /**
701
     * Called at the start of an element.
702
     *
703
     * @param array<string> $attrs an array of key value pairs for the attributes
704
     *
705
     * @return void
706
     */
707
    protected function gedcomStartHandler(array $attrs): void
708
    {
709
        if ($this->process_gedcoms > 0) {
710
            $this->process_gedcoms++;
711
712
            return;
713
        }
714
715
        $tag       = $attrs['id'];
716
        $tag       = str_replace('@fact', $this->fact, $tag);
717
        $tags      = explode(':', $tag);
718
        $newgedrec = '';
719
        if (count($tags) < 2) {
720
            $tmp       = Registry::gedcomRecordFactory()->make($attrs['id'], $this->tree);
721
            $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($this->tree)) : '';
722
        }
723
        if (empty($newgedrec)) {
724
            $tgedrec   = $this->gedrec;
725
            $newgedrec = '';
726
            foreach ($tags as $tag) {
727
                if (preg_match('/\$(.+)/', $tag, $match)) {
728
                    if (isset($this->vars[$match[1]]['gedcom'])) {
729
                        $newgedrec = $this->vars[$match[1]]['gedcom'];
730
                    } else {
731
                        $tmp       = Registry::gedcomRecordFactory()->make($match[1], $this->tree);
732
                        $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($this->tree)) : '';
733
                    }
734
                } else {
735
                    if (preg_match('/@(.+)/', $tag, $match)) {
736
                        $gmatch = [];
737
                        if (preg_match("/\d $match[1] @([^@]+)@/", $tgedrec, $gmatch)) {
738
                            $tmp       = Registry::gedcomRecordFactory()->make($gmatch[1], $this->tree);
739
                            $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($this->tree)) : '';
740
                            $tgedrec   = $newgedrec;
741
                        } else {
742
                            $newgedrec = '';
743
                            break;
744
                        }
745
                    } else {
746
                        $level     = 1 + (int) explode(' ', trim($tgedrec))[0];
747
                        $newgedrec = self::getSubRecord($level, "$level $tag", $tgedrec);
748
                        $tgedrec   = $newgedrec;
749
                    }
750
                }
751
            }
752
        }
753
        if (!empty($newgedrec)) {
754
            $this->gedrec_stack[] = [$this->gedrec, $this->fact, $this->desc];
755
            $this->gedrec         = $newgedrec;
756
            if (preg_match("/(\d+) (_?[A-Z0-9]+) (.*)/", $this->gedrec, $match)) {
757
                $this->fact = $match[2];
758
                $this->desc = trim($match[3]);
759
            }
760
        } else {
761
            $this->process_gedcoms++;
762
        }
763
    }
764
765
    /**
766
     * Called at the end of an element.
767
     *
768
     * @return void
769
     */
770
    protected function gedcomEndHandler(): void
771
    {
772
        if ($this->process_gedcoms > 0) {
773
            $this->process_gedcoms--;
774
        } else {
775
            [$this->gedrec, $this->fact, $this->desc] = array_pop($this->gedrec_stack);
776
        }
777
    }
778
779
    /**
780
     * Handle <textBox>
781
     *
782
     * @param array<string> $attrs
783
     *
784
     * @return void
785
     */
786
    protected function textBoxStartHandler(array $attrs): void
787
    {
788
        // string Background color code
789
        $bgcolor = '';
790
        if (!empty($attrs['bgcolor'])) {
791
            $bgcolor = $attrs['bgcolor'];
792
        }
793
794
        // boolean Wether or not fill the background color
795
        $fill = true;
796
        if (isset($attrs['fill'])) {
797
            if ($attrs['fill'] === '0') {
798
                $fill = false;
799
            } elseif ($attrs['fill'] === '1') {
800
                $fill = true;
801
            }
802
        }
803
804
        // var boolean Whether or not a border should be printed around this box. 0 = no border, 1 = border. Default is 0
805
        $border = false;
806
        if (isset($attrs['border'])) {
807
            if ($attrs['border'] === '1') {
808
                $border = true;
809
            } elseif ($attrs['border'] === '0') {
810
                $border = false;
811
            }
812
        }
813
814
        // int The starting height of this cell. If the text wraps the height will automatically be adjusted
815
        $height = 0;
816
        if (!empty($attrs['height'])) {
817
            $height = (int) $attrs['height'];
818
        }
819
        // int Setting the width to 0 will make it the width from the current location to the margin
820
        $width = 0;
821
        if (!empty($attrs['width'])) {
822
            $width = (int) $attrs['width'];
823
        }
824
825
        // mixed Position the left corner of this box on the page. The default is the current position.
826
        $left = ReportBaseElement::CURRENT_POSITION;
827
        if (isset($attrs['left'])) {
828
            if ($attrs['left'] === '.') {
829
                $left = ReportBaseElement::CURRENT_POSITION;
830
            } elseif (!empty($attrs['left'])) {
831
                $left = (int) $attrs['left'];
832
            } elseif ($attrs['left'] === '0') {
833
                $left = 0;
834
            }
835
        }
836
        // mixed Position the top corner of this box on the page. the default is the current position
837
        $top = ReportBaseElement::CURRENT_POSITION;
838
        if (isset($attrs['top'])) {
839
            if ($attrs['top'] === '.') {
840
                $top = ReportBaseElement::CURRENT_POSITION;
841
            } elseif (!empty($attrs['top'])) {
842
                $top = (int) $attrs['top'];
843
            } elseif ($attrs['top'] === '0') {
844
                $top = 0;
845
            }
846
        }
847
        // position of box absolute or relative, and possibly top and height
848
        $abs_position = false;
849
        $top_position = $top;
850
        if (isset($attrs['pos'])) {
851
            $pos = $attrs['pos'];
852
            if (substr($pos, 0, 3) == 'abs') {
853
                $abs_position = true;
854
            }
855
            if (substr($pos, 0, 3) == 'rel') {
856
                $abs_position = false;
857
                $top_position = 0;
858
            }
859
        }
860
861
        if (isset($attrs['top_adj'])) {
862
            if (!empty($attrs['top_adj'])) {
863
                $top_position = (int) $attrs['top_adj'];
864
            }
865
        }
866
867
        error_log("\nRPG:867 [abs_position,top_position,top]=".var_export([$abs_position,$top_position,$top],true)."\n",3,"rpg.log");
868
        // 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
869
870
        $newline = false;
871
        if (isset($attrs['newline'])) {
872
            if ($attrs['newline'] === '1') {
873
                $newline = true;
874
            } elseif ($attrs['newline'] === '0') {
875
                $newline = false;
876
            }
877
        }
878
        // boolean
879
        $pagecheck = true;
880
        if (isset($attrs['pagecheck'])) {
881
            if ($attrs['pagecheck'] === '0') {
882
                $pagecheck = false;
883
            } elseif ($attrs['pagecheck'] === '1') {
884
                $pagecheck = true;
885
            }
886
        }
887
        // boolean Cell padding
888
        $padding = true;
889
        if (isset($attrs['padding'])) {
890
            if ($attrs['padding'] === '0') {
891
                $padding = false;
892
            } elseif ($attrs['padding'] === '1') {
893
                $padding = true;
894
            }
895
        }
896
        // boolean Reset this box Height
897
        $reseth = false;
898
        if (isset($attrs['reseth'])) {
899
            if ($attrs['reseth'] === '1') {
900
                $reseth = true;
901
            } elseif ($attrs['reseth'] === '0') {
902
                $reseth = false;
903
            }
904
        }
905
906
        // string Style of rendering
907
        $style = '';
908
909
        $this->print_data_stack[] = $this->print_data;
910
        $this->print_data         = false;
911
912
        $this->wt_report_stack[] = $this->wt_report;
913
        $this->wt_report         = $this->report_root->createTextBox(
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->report_root->crea...osition, $top_position) of type Fisharebest\Webtrees\Report\ReportBaseTextbox is incompatible with the declared type Fisharebest\Webtrees\Report\AbstractRenderer of property $wt_report.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
914
            $width,
915
            $height,
916
            $border,
917
            $bgcolor,
918
            $newline,
919
            $left,
920
            $top,
921
            $pagecheck,
922
            $style,
923
            $fill,
924
            $padding,
925
            $reseth,
926
            $abs_position,
927
            $top_position
928
        );
929
    }
930
931
    /**
932
     * Handle <textBox>
933
     *
934
     * @return void
935
     */
936
    protected function textBoxEndHandler(): void
937
    {
938
        $this->print_data      = array_pop($this->print_data_stack);
939
        $this->current_element = $this->wt_report;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->wt_report of type Fisharebest\Webtrees\Report\AbstractRenderer is incompatible with the declared type Fisharebest\Webtrees\Report\ReportBaseElement of property $current_element.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
940
941
        // The TextBox handler is mis-using the wt_report attribute to store an element.
942
        // Until this can be re-designed, we need this assertion to help static analysis tools.
943
        assert($this->current_element instanceof ReportBaseElement, new LogicException());
944
945
        $this->wt_report = array_pop($this->wt_report_stack);
946
        $this->wt_report->addElement($this->current_element);
947
    }
948
949
    /**
950
     * XLM <Text>.
951
     *
952
     * @param array<string> $attrs an array of key value pairs for the attributes
953
     *
954
     * @return void
955
     */
956
    protected function textStartHandler(array $attrs): void
957
    {
958
        $this->print_data_stack[] = $this->print_data;
959
        $this->print_data         = true;
960
961
        // string The name of the Style that should be used to render the text.
962
        $style = '';
963
        if (isset($attrs['style'])) {
964
            $style = $attrs['style'];
965
        }
966
967
        // string  The color of the text - Keep the black color as default
968
        $color = '';
969
        if (isset($attrs['color'])) {
970
            $color = $attrs['color'];
971
        }
972
973
        $this->current_element = $this->report_root->createText($style, $color);
974
    }
975
976
    /**
977
     * Handle </text>
978
     *
979
     * @return void
980
     */
981
    protected function textEndHandler(): void
982
    {
983
        $this->print_data = array_pop($this->print_data_stack);
984
        $this->wt_report->addElement($this->current_element);
985
    }
986
987
    /**
988
     * Handle <getPersonName />
989
     * Get the name
990
     * 1. id is empty - current GEDCOM record
991
     * 2. id is set with a record id
992
     *
993
     * @param array<string> $attrs an array of key value pairs for the attributes
994
     *
995
     * @return void
996
     */
997
    protected function getPersonNameStartHandler(array $attrs): void
998
    {
999
        $id    = '';
1000
        $match = [];
1001
        if (empty($attrs['id'])) {
1002
            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1003
                $id = $match[1];
1004
            }
1005
        } else {
1006
            if (preg_match('/\$(.+)/', $attrs['id'], $match)) {
1007
                if (isset($this->vars[$match[1]]['id'])) {
1008
                    $id = $this->vars[$match[1]]['id'];
1009
                }
1010
            } else {
1011
                if (preg_match('/@(.+)/', $attrs['id'], $match)) {
1012
                    $gmatch = [];
1013
                    if (preg_match("/\d $match[1] @([^@]+)@/", $this->gedrec, $gmatch)) {
1014
                        $id = $gmatch[1];
1015
                    }
1016
                } else {
1017
                    $id = $attrs['id'];
1018
                }
1019
            }
1020
        }
1021
        $nameselect = "";
1022
        if (isset($attrs['select'])) {
1023
            $nameselect = $attrs['select'];
1024
        }
1025
        $namesep = "";
1026
        if (isset($attrs['name_sep'])) {
1027
            $namesep = $attrs['name_sep'];
1028
        }
1029
        $famrel = false;
1030
        if (isset($attrs['fam_relation'])) {
1031
            $famrel = true;
1032
        }
1033
        if (!empty($id)) {
1034
            $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1035
            if ($record === null) {
1036
                return;
1037
            }
1038
            if (!$record->canShowName()) {
1039
                $this->current_element->addText(I18N::translate('Private'));
1040
            } elseif ($nameselect == 'latest') {
1041
                $tmp = $record->getAllNames();
1042
                $name  = strip_tags($tmp[count($tmp) - 1]['full']);
1043
                $this->current_element->addText(trim($name));
1044
            } elseif ($nameselect == 'combined') {
1045
                $tmp = $record->getAllNames();
1046
                $name = $tmp[count($tmp) - 1]['full'];
1047
                $ix1 = strpos($name, '<span class="starredname">');
1048
                if ($ix1 !== false) {   // '«' and '»' mark text for underlining
1049
                    $name = substr_replace($name, '«', $ix1, 26);
1050
                    $ix1 = strpos($name, '</span>', $ix1);
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1050
                    $ix1 = strpos(/** @scrutinizer ignore-type */ $name, '</span>', $ix1);
Loading history...
1051
                    if ($ix1 !== false) {   // '«' and '»' mark text for underlining
1052
                        $name = substr_replace($name, '»', $ix1, 7);
1053
                    }
1054
                }
1055
                $addname = strip_tags((string) $tmp[0]['surn']);
1056
                if (!empty($addname) && !($addname === '@N.N.') && !str_contains($name, $addname)) {
1057
                    $name .= " " . $namesep . " " . $addname;
1058
                }
1059
                $this->current_element->addText(trim($name));
1060
            } else {
1061
                $name = $record->fullName();
1062
                $name = strip_tags($name);
1063
                if (!empty($attrs['truncate'])) {
1064
                    if ((int) $attrs['truncate'] > 0) {
1065
                        $name = Str::limit($name, (int) $attrs['truncate'], I18N::translate('…'));
1066
                    }
1067
                } else {
1068
                    $addname = (string) $record->alternateName();
1069
                    $addname = strip_tags($addname);
1070
                    if (!empty($addname)) {
1071
                        $name .= ' ' . $addname;
1072
                    }
1073
                }
1074
                $this->current_element->addText(trim($name));
1075
            }
1076
        }
1077
        if (isset($record) && $famrel && ($this->mfrelation[$record->xref()] != "")) {
1078
            $this->current_element->addText(" (" . (string) $this->mfrelation[$record->xref()] . ")");
1079
        }
1080
    }
1081
1082
    /**
1083
     * Handle <gedcomValue />
1084
     *
1085
     * @param array<string> $attrs
1086
     *
1087
     * @return void
1088
     */
1089
    protected function gedcomValueStartHandler(array $attrs): void
1090
    {
1091
        $id    = '';
1092
        $match = [];
1093
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1094
            $id = $match[1];
1095
        }
1096
1097
        if (isset($attrs['newline']) && $attrs['newline'] === '1') {
1098
            $useBreak = '1';
1099
        } else {
1100
            $useBreak = '0';
1101
        }
1102
1103
        $tag = $attrs['tag'];
1104
        if (!empty($tag)) {
1105
            if ($tag === '@desc') {
1106
                $value = $this->desc;
1107
                $value = trim($value);
1108
                $this->current_element->addText($value);
1109
            }
1110
            if ($tag === '@id') {
1111
                $this->current_element->addText($id);
1112
            } else {
1113
                $tag = str_replace('@fact', $this->fact, $tag);
1114
                if (empty($attrs['level'])) {
1115
                    $level = (int) explode(' ', trim($this->gedrec))[0];
1116
                    if ($level === 0) {
1117
                        $level++;
1118
                    }
1119
                } else {
1120
                    $level = (int) $attrs['level'];
1121
                }
1122
                $tags  = preg_split('/[: ]/', $tag);
1123
                $value = $this->getGedcomValue($tag, $level, $this->gedrec);
1124
                switch (end($tags)) {
1125
                    case 'DATE':
1126
                        $tmp   = new Date($value);
1127
                        $dfmt = "%j %F %Y";
1128
                        if (!empty($attrs['truncate'])) {
1129
                            if ($attrs['truncate'] === "d") {
1130
                                $dfmt = "%j %M %Y";
1131
                            }
1132
                            if ($attrs['truncate'] === "Y") {
1133
                                $dfmt = "%Y";
1134
                            }
1135
                        }
1136
                        $value = strip_tags($tmp->display(null, $dfmt));
1137
                        break;
1138
                    case 'PLAC':
1139
                        $tmp   = new Place($value, $this->tree);
1140
                        $value = $tmp->shortName();
1141
                        break;
1142
                }
1143
                if ($useBreak === '1') {
1144
                    // Insert <br> when multiple dates exist.
1145
                    // This works around a TCPDF bug that incorrectly wraps RTL dates on LTR pages
1146
                    $value = str_replace('(', '<br>(', $value);
1147
                    $value = str_replace('<span dir="ltr"><br>', '<br><span dir="ltr">', $value);
1148
                    $value = str_replace('<span dir="rtl"><br>', '<br><span dir="rtl">', $value);
1149
                    if (substr($value, 0, 4) === '<br>') {
1150
                        $value = substr($value, 4);
1151
                    }
1152
                }
1153
                $tmp = explode(':', $tag);
1154
                if (in_array(end($tmp), ['NOTE', 'TEXT'], true)) {
1155
                    if ($this->tree->getPreference('FORMAT_TEXT') === 'xxmarkdown') {
1156
                        $value = strip_tags(Registry::markdownFactory()->markdown($value, $this->tree), ['br']);
1157
                    } else {
1158
                        $value = str_replace("\n", "<br>", $value);
1159
                        //$value = strip_tags(Registry::markdownFactory()->autolink($value, $this->tree), ['br']);
1160
                    }
1161
                    $value = strtr($value, [MarkdownFactory::BREAK => ' ']);
1162
                }
1163
1164
                if (isset($attrs['lcfirst'])) {
1165
                    $value = lcfirst($value);
1166
                    $value = str_replace(["Å","Ä","Ö"], ["å","ä","ö"], $value);
1167
                }
1168
1169
                if (!empty($attrs['truncate'])) {
1170
                    $value = strip_tags($value);
1171
                    if ((int) $attrs['truncate'] > 0) {
1172
                        $value = Str::limit($value, (int) $attrs['truncate'], I18N::translate('…'));
1173
                    }
1174
                }
1175
                $this->current_element->addText($value);
1176
            }
1177
        }
1178
    }
1179
1180
    /**
1181
     * Handle <repeatTag>
1182
     *
1183
     * @param array<string> $attrs
1184
     *
1185
     * @return void
1186
     */
1187
    protected function repeatTagStartHandler(array $attrs): void
1188
    {
1189
        $this->process_repeats++;
1190
        if ($this->process_repeats > 1) {
1191
            return;
1192
        }
1193
1194
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
1195
        $this->repeats         = [];
1196
        $this->repeat_bytes    = xml_get_current_line_number($this->parser);
1197
1198
        $tag = $attrs['tag'] ?? '';
1199
        if (!empty($tag)) {
1200
            if ($tag === '@desc') {
1201
                $value = $this->desc;
1202
                $value = trim($value);
1203
                $this->current_element->addText($value);
1204
            } else {
1205
                $tag   = str_replace('@fact', $this->fact, $tag);
1206
                $tags  = explode(':', $tag);
1207
                $level = (int) explode(' ', trim($this->gedrec))[0];
1208
                if ($level === 0) {
1209
                    $level++;
1210
                }
1211
                $subrec = $this->gedrec;
1212
                $t      = $tag;
1213
                $count  = count($tags);
1214
                $i      = 0;
1215
                while ($i < $count) {
1216
                    $t = $tags[$i];
1217
                    if (!empty($t)) {
1218
                        if ($i < ($count - 1)) {
1219
                            $subrec = self::getSubRecord($level, "$level $t", $subrec);
1220
                            if (empty($subrec)) {
1221
                                $level--;
1222
                                $subrec = self::getSubRecord($level, "@ $t", $this->gedrec);
1223
                                if (empty($subrec)) {
1224
                                    return;
1225
                                }
1226
                            }
1227
                        }
1228
                        $level++;
1229
                    }
1230
                    $i++;
1231
                }
1232
                $level--;
1233
                $count = preg_match_all("/$level $t(.*)/", $subrec, $match, PREG_SET_ORDER);
1234
                $i     = 0;
1235
                while ($i < $count) {
1236
                    $i++;
1237
                    // Privacy check - is this a link, and are we allowed to view the linked object?
1238
                    $subrecord = self::getSubRecord($level, "$level $t", $subrec, $i);
1239
                    if (preg_match('/^\d ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@/', $subrecord, $xref_match)) {
1240
                        $linked_object = Registry::gedcomRecordFactory()->make($xref_match[1], $this->tree);
1241
                        if ($linked_object && !$linked_object->canShow()) {
1242
                            //continue;
1243
                        }
1244
                    }
1245
                    $this->repeats[] = $subrecord;
1246
                }
1247
            }
1248
        }
1249
    }
1250
1251
    /**
1252
     * Handle </repeatTag>
1253
     *
1254
     * @return void
1255
     */
1256
    protected function repeatTagEndHandler(): void
1257
    {
1258
        $this->process_repeats--;
1259
        if ($this->process_repeats > 0) {
1260
            return;
1261
        }
1262
1263
        $nnnn = count($this->repeats);
0 ignored issues
show
Unused Code introduced by
The assignment to $nnnn is dead and can be removed.
Loading history...
1264
        $rpt1 = isset($this->repeats[0]) ? $this->repeats[0] : "";
0 ignored issues
show
Unused Code introduced by
The assignment to $rpt1 is dead and can be removed.
Loading history...
1265
        // Check if there is anything to repeat
1266
        if (count($this->repeats) > 0) {
1267
            // No need to load them if not used...
1268
1269
            //-- read the xml from the file
1270
            $lines = file($this->report);
1271
            if (empty($lines)) {
1272
                error_log(__FILE__ . ":" . __LINE__ . " impossible error!? \n");
1273
                // this can not happen! phpstan forces me to add stupid code
1274
                // we come here because the xml file exists! Is it possible to tell phpstan the $lines *is* an array ??
1275
                die("can not happen!!!");
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1276
            }
1277
            $lineoffset = 0;
1278
            foreach ($this->repeats_stack as $rep) {
1279
                if (!empty($rep[1])) {
1280
                    $lineoffset = $lineoffset + (int) ($rep[1]) - 1;
1281
                }
1282
            }
1283
            while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<RepeatTag')) {
1284
                $lineoffset--;
1285
            }
1286
            $lineoffset++;
1287
            $reportxml = "<tempdoc>\n";
1288
            $line_nr   = $lineoffset + $this->repeat_bytes;
1289
            $lnnn = $line_nr;
0 ignored issues
show
Unused Code introduced by
The assignment to $lnnn is dead and can be removed.
Loading history...
1290
            // RepeatTag Level counter
1291
            $count = 1;
1292
            while (0 < $count) {
1293
                if (str_contains($lines[$line_nr], '<RepeatTag')) {
1294
                    $count++;
1295
                } elseif (str_contains($lines[$line_nr], '</RepeatTag')) {
1296
                    $count--;
1297
                }
1298
                if (0 < $count) {
1299
                    $reportxml .= $lines[$line_nr];
1300
                }
1301
                $line_nr++;
1302
            }
1303
            // No need to drag this
1304
            unset($lines);
1305
            $reportxml .= "</tempdoc>\n";
1306
            // Save original values
1307
            $this->parser_stack[] = $this->parser;
1308
            $oldgedrec            = $this->gedrec;
1309
            foreach ($this->repeats as $gedrec) {
1310
                $this->gedrec  = $gedrec;
1311
                $repeat_parser = xml_parser_create();
1312
                $this->parser  = $repeat_parser;
1313
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
1314
1315
                xml_set_element_handler(
1316
                    $repeat_parser,
1317
                    function ($parser, string $name, array $attrs): void {
1318
                        $this->startElement($parser, $name, $attrs);
1319
                    },
1320
                    function ($parser, string $name): void {
1321
                        $this->endElement($parser, $name);
1322
                    }
1323
                );
1324
1325
                xml_set_character_data_handler(
1326
                    $repeat_parser,
1327
                    function ($parser, string $data): void {
1328
                        $this->characterData($parser, $data);
1329
                    }
1330
                );
1331
1332
                if (!xml_parse($repeat_parser, $reportxml, true)) {
1333
                    throw new DomainException(sprintf(
1334
                        'RepeatTagEHandler XML error: %s at line %d',
1335
                        xml_error_string(xml_get_error_code($repeat_parser)),
1336
                        xml_get_current_line_number($repeat_parser)
1337
                    ));
1338
                }
1339
                xml_parser_free($repeat_parser);
1340
            }
1341
            // Restore original values
1342
            $this->gedrec = $oldgedrec;
1343
            $this->parser = array_pop($this->parser_stack);
1344
        }
1345
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
1346
    }
1347
1348
    /**
1349
     * Variable lookup
1350
     * Retrieve predefined variables :
1351
     * @ desc GEDCOM fact description, example:
1352
     *        1 EVEN This is a description
1353
     * @ fact GEDCOM fact tag, such as BIRT, DEAT etc.
1354
     * $ I18N::translate('....')
1355
     * $ language_settings[]
1356
     *
1357
     * @param array<string> $attrs an array of key value pairs for the attributes
1358
     *
1359
     * @return void
1360
     */
1361
    protected function varStartHandler(array $attrs): void
1362
    {
1363
        if (!isset($attrs['var'])) {
1364
            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));
1365
        }
1366
1367
        $var = $attrs['var'];
1368
        // SetVar element preset variables
1369
        if (!empty($this->vars[$var]['id'])) {
1370
            $var = $this->vars[$var]['id'];
1371
        } else {
1372
            $tfact = $this->fact;
1373
            if (($this->fact === 'EVEN' || $this->fact === 'FACT') && $this->type !== '') {
1374
                // Use :
1375
                // n TYPE This text if string
1376
                $tfact = $this->type;
1377
            } else {
1378
                foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) {
1379
                    $element = Registry::elementFactory()->make($record_type . ':' . $this->fact);
1380
1381
                    if (!$element instanceof UnknownElement) {
1382
                        $tfact = $element->label();
1383
                        break;
1384
                    }
1385
                }
1386
            }
1387
1388
            $var = strtr($var, ['@desc' => $this->desc, '@fact' => $tfact]);
1389
1390
            if (preg_match('/^I18N::number\((.+)\)$/', $var, $match)) {
1391
                $var = I18N::number((int) $match[1]);
1392
            } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $var, $match)) {
1393
                $var = I18N::translate($match[1]);
1394
            } elseif (preg_match('/^I18N::translate\(\$(.+)\)$/', $var, $match)) {
1395
                $var = I18N::translate($this->vars[$match[1]]['id']);
1396
            } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $var, $match)) {
1397
                $var = I18N::translateContext($match[1], $match[2]);
1398
            }
1399
        }
1400
        // Check if variable is set as a date and reformat the date
1401
        if (isset($attrs['date'])) {
1402
            if ($attrs['date'] === '1') {
1403
                $g   = new Date($var);
1404
                $var = $g->display();
1405
            }
1406
        }
1407
        if (isset($attrs['amp'])) {
1408
            $var = str_replace("%26", '&', $var);
1409
        }
1410
        if (isset($attrs['cut'])) {
1411
            $cut = (int) $attrs['cut'];
1412
            $var = $cut > 0 ? substr($var, 0, $cut) : substr($var, $cut);
1413
            if ($cut == 0) {
1414
                $var = "";
1415
            }
1416
        }
1417
        if (isset($attrs['lcfirst'])) {
1418
            $var = lcfirst($var);
1419
        }
1420
        $this->current_element->addText($var);
1421
        $this->text = $var; // Used for title/description
1422
    }
1423
1424
    /**
1425
     * Handle <facts>
1426
     *
1427
     * @param array<string> $attrs
1428
     *
1429
     * @return void
1430
     */
1431
    protected function factsStartHandler(array $attrs): void
1432
    {
1433
        $this->process_repeats++;
1434
        if ($this->process_repeats > 1) {
1435
            return;
1436
        }
1437
1438
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
1439
        $this->repeats         = [];
1440
        $this->repeat_bytes    = xml_get_current_line_number($this->parser);
1441
1442
        $id    = '';
1443
        $match = [];
1444
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1445
            $id = $match[1];
1446
        }
1447
        $tag = '';
1448
        if (isset($attrs['ignore'])) {
1449
            $tag .= $attrs['ignore'];
1450
        }
1451
        if (preg_match('/\$(.+)/', $tag, $match)) {
1452
            $tag = $this->vars[$match[1]]['id'];
1453
        }
1454
1455
        $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1456
        if (empty($attrs['diff']) && !empty($id)) {
1457
            $facts = $record->facts([], true);
1458
            $this->repeats = [];
1459
            $nonfacts      = explode(',', $tag);
1460
            foreach ($facts as $fact) {
1461
                $tag = explode(':', $fact->tag())[1];
1462
1463
                if (!in_array($tag, $nonfacts, true)) {
1464
                    $this->repeats[] = $fact->gedcom();
1465
                }
1466
            }
1467
        } else {
1468
            foreach ($record->facts() as $fact) {
1469
                if (($fact->isPendingAddition() || $fact->isPendingDeletion()) && !str_ends_with($fact->tag(), ':CHAN')) {
1470
                    $this->repeats[] = $fact->gedcom();
1471
                }
1472
            }
1473
        }
1474
1475
        $jdarr = [];
1476
        // Add fact/event for FAM:DIV and for death of spouse
1477
        foreach ($this->repeats as $key => $fact) {
1478
            $jdarr[$key] = 0;
1479
            if (preg_match('/1 FAMS @(.+)@/', $fact, $match)) {
1480
                $famid = $match[1];
1481
                $fam = Registry::familyFactory()->make($match[1], $this->tree);
1482
                if ($fam === null) {
1483
                    continue;
1484
                }
1485
                $dt = $this->getGedcomValue("MARR:DATE", 0, $fam->gedcom());
1486
                if ($dt == "") {
1487
                    $dt = $this->getGedcomValue("ENGA:DATE", 0, $fam->gedcom());
1488
                }
1489
                if ($dt == "" && $this->getGedcomValue("EVEN:TYPE", 0, $fam->gedcom()) == "Sambo") {
1490
                    $dt = $this->getGedcomValue("EVEN:DATE", 0, $fam->gedcom());
1491
                }
1492
                $date = new Date($dt);
1493
                $jd = $date->julianDay();
1494
                $jdarr[$key] = $jd;
1495
                // Divorce
1496
                $dt = $this->getGedcomValue("DIV:DATE", 0, $fam->gedcom());
1497
                if ($dt != "") {
1498
                    $this->repeats[] = "1 DIV\n2 DATE " . $dt . "\n";
1499
                }
1500
                // Separation // Doesn't work!! getGedComValue only reports the first event!! I.e. no match here
1501
                if ($this->getGedcomValue("EVEN:TYPE", 0, $fam->gedcom()) == "Separation") {
1502
                    $dt = $this->getGedcomValue("EVEN:DATE", 0, $fam->gedcom());
1503
                    if ($dt != "") {
1504
                        $this->repeats[] = "1 EVEN\n2 TYPE Separation\n2 DATE " . $dt . "\n";
1505
                    }
1506
                }
1507
                // death of husband / wife
1508
                $husb = $fam->husband();
1509
                $wife = $fam->wife();
1510
                if ($this->getGedcomValue("SEX", 0, $this->gedrec) == "M") {
1511
                    $spouse = $wife;
1512
                } else {
1513
                    $spouse = $husb;
1514
                }
1515
                if ($spouse) {
1516
                    $dt = $this->getGedcomValue("DEAT:DATE", 0, $spouse->gedcom());
1517
                } else {
1518
                    $dt = "";
1519
                }
1520
                if ($dt != "") {
1521
                    $this->repeats[] = "1 _SP_DEAT\n2 DATE " . $dt . "\n2 _O_FAM " . $famid . "\n";
1522
                }
1523
            }
1524
        }
1525
        // Find the dates for the facts that are found
1526
        foreach ($this->repeats as $key => $fact) {
1527
            if (preg_match('/[234] DATE ([^\n]+)/', $fact, $match)) {
1528
                $date = new Date($match[1]);
1529
                $jd = $date->julianDay();
1530
                $jdarr[$key] = $jd;
1531
            }
1532
        }
1533
1534
        // Sort facts in chronological order, if possible
1535
        $m = count($this->repeats) - 1;
1536
        $prevd = 0;
1537
        for ($i = 0; $i <= $m; $i++) { // keep undated events after previous dated event
1538
            if ($jdarr[$i] === 0) {
1539
                $jdarr[$i] = $prevd;
1540
            } else {
1541
                $prevd = $jdarr[$i];
1542
            }
1543
        }
1544
1545
        while ($m > 1) {
1546
            $n = count($this->repeats);
1547
            while ($n > 1) {
1548
                if ($jdarr[$n - 2] > $jdarr[$n - 1] && $jdarr[$n - 1] !== 0) {
1549
                    $s = $this->repeats[$n - 1];
1550
                    $this->repeats[$n - 1] = $this->repeats[$n - 2];
1551
                    $this->repeats[$n - 2] = $s;
1552
                    $s = $jdarr[$n - 1];
1553
                    $jdarr[$n - 1] = $jdarr[$n - 2];
1554
                    $jdarr[$n - 2] = $s;
1555
                }
1556
                $n -= 1;
1557
            }
1558
            $m -= 1;
1559
        }
1560
1561
        // Remove spouse deaths that are too late: after new marriage or own death
1562
        $currfam = "";
1563
        for ($i = 0; $i <= count($this->repeats) - 1; $i++) {
1564
            if (preg_match('/[1234] FAMS @(.+)@/', $this->repeats[$i], $match)) {
1565
                $currfam = $match[1];
1566
            }
1567
            if (preg_match('/_SP_DEAT.*\n2 DATE (.*)\n.*_O_FAM (.+)\n/', $this->repeats[$i], $match)) {
1568
                if ($currfam != $match[2] || $i == count($this->repeats) - 1) {
1569
                    $this->repeats[$i] = "1 _XXX\n";
1570
                } // ignore fact
1571
            }
1572
        }
1573
    }
1574
1575
    /**
1576
     * Handle </facts>
1577
     *
1578
     * @return void
1579
     */
1580
    protected function factsEndHandler(): void
1581
    {
1582
        $this->process_repeats--;
1583
        if ($this->process_repeats > 0) {
1584
            return;
1585
        }
1586
1587
        // Check if there is anything to repeat
1588
        if (count($this->repeats) > 0) {
1589
            $line       = xml_get_current_line_number($this->parser) - 1;
1590
            $lineoffset = 0;
1591
            foreach ($this->repeats_stack as $rep) {
1592
                $lineoffset = $lineoffset + (int) ($rep[1]) - 1;
1593
            }
1594
1595
            //-- read the xml from the file
1596
            $lines = file($this->report);
1597
            if (empty($lines)) {
1598
                error_log(__FILE__ . ":" . __LINE__ . " impossible error!? \n");
1599
                // this can not happen! phpstan forces me to add stupid code
1600
                // we come here because the xml file exists! Is it possible to tell phpstan the $lines *is* an array ??
1601
                die("can not happen!!!");
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1602
            }
1603
            while ($lineoffset + $this->repeat_bytes > 0 && !str_contains($lines[$lineoffset + $this->repeat_bytes], '<Facts ')) {
1604
                $lineoffset--;
1605
            }
1606
            $lineoffset++;
1607
            $reportxml = "<tempdoc>\n";
1608
            $i         = $line + $lineoffset;
1609
            $line_nr   = $this->repeat_bytes + $lineoffset;
1610
            while ($line_nr < $i) {
1611
                $reportxml .= $lines[$line_nr];
1612
                $line_nr++;
1613
            }
1614
            // No need to drag this
1615
            unset($lines);
1616
            $reportxml .= "</tempdoc>\n";
1617
            // Save original values
1618
            $this->parser_stack[] = $this->parser;
1619
            $oldgedrec = $this->gedrec;
1620
            $count = count($this->repeats);
1621
            $i = 0;
1622
            while ($i < $count) {
1623
                if (!isset($this->repeats[$i])) {
1624
                    $i++;
1625
                    continue; // this fact has been removed above, occured too late
1626
                }
1627
                $this->gedrec = $this->repeats[$i];
1628
                $this->fact = '';
1629
                $this->desc = '';
1630
                if (preg_match('/1 (\w+)(.*)/', $this->gedrec, $match)) {
1631
                    $this->fact = $match[1];
1632
                    if ($this->fact === 'EVEN' || $this->fact === 'FACT') {
1633
                        $tmatch = [];
1634
                        if (preg_match('/2 TYPE (.+)/', $this->gedrec, $tmatch)) {
1635
                            $this->type = trim($tmatch[1]);
1636
                        } else {
1637
                            $this->type = ' ';
1638
                        }
1639
                    }
1640
                    $this->desc = trim($match[2]);
1641
                    $this->desc .= self::getCont(2, $this->gedrec);
1642
                }
1643
                $repeat_parser = xml_parser_create();
1644
                $this->parser  = $repeat_parser;
1645
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
1646
1647
                xml_set_element_handler(
1648
                    $repeat_parser,
1649
                    function ($parser, string $name, array $attrs): void {
1650
                        $this->startElement($parser, $name, $attrs);
1651
                    },
1652
                    function ($parser, string $name): void {
1653
                        $this->endElement($parser, $name);
1654
                    }
1655
                );
1656
1657
                xml_set_character_data_handler(
1658
                    $repeat_parser,
1659
                    function ($parser, string $data): void {
1660
                        $this->characterData($parser, $data);
1661
                    }
1662
                );
1663
1664
                if (!xml_parse($repeat_parser, $reportxml, true)) {
1665
                    throw new DomainException(sprintf(
1666
                        'FactsEHandler XML error: %s at line %d',
1667
                        xml_error_string(xml_get_error_code($repeat_parser)),
1668
                        xml_get_current_line_number($repeat_parser)
1669
                    ));
1670
                }
1671
                xml_parser_free($repeat_parser);
1672
                $i++;
1673
            }
1674
            // Restore original values
1675
            $this->parser = array_pop($this->parser_stack);
1676
            $this->gedrec = $oldgedrec;
1677
        }
1678
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
1679
    }
1680
1681
    /**
1682
     * Setting upp or changing variables in the XML
1683
     * The XML variable name and value is stored in $this->vars
1684
     *
1685
     * @param array<string> $attrs an array of key value pairs for the attributes
1686
     *
1687
     * @return void
1688
     */
1689
    protected function setVarStartHandler(array $attrs): void
1690
    {
1691
        if (empty($attrs['name'])) {
1692
            throw new DomainException('REPORT ERROR var: The attribute "name" is missing or not set in the XML file');
1693
        }
1694
1695
        $name  = $attrs['name'];
1696
        $value = $attrs['value'];
1697
        if (isset($attrs['dumpvar'])) {
1698
            $dumpvar = $attrs['dumpvar'];
1699
        } else {
1700
            $dumpvar = "";
1701
        }
1702
        $curr_id = "";
1703
        $match = [];
1704
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1705
            $curr_id = $match[1];
1706
        }
1707
        $match = [];
1708
        // Current GEDCOM record strings
1709
        if ($value === '@ID') {
1710
            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1711
                $value = $match[1];
1712
            }
1713
        } elseif ($value === '@fact') {
1714
            $value = $this->fact;
1715
        } elseif ($value === '@desc') {
1716
            $value = $this->desc;
1717
        } elseif ($value === '@format') {
1718
            if (isset($_GET["format"])) {
1719
                $value = $_GET["format"];
1720
            } else {
1721
                $value = "";
1722
            }
1723
        } elseif ($value === '@generation') {
1724
            $value = (string) $this->generation;
1725
        } elseif ($value === '@base_url') {
1726
            $value = "";
1727
            if (array_key_exists("REQUEST_URI", $_SERVER)) {
1728
                $value = urldecode($_SERVER["REQUEST_URI"]);
1729
            }
1730
            $url1 = "";
1731
            $i = strpos($value, "route=");
1732
            if ($i !== false) {
1733
                $url1 = substr($value, 0, $i + 6);
1734
                $value = substr($value, $i + 6);
1735
            }
1736
            $i = strpos($value, "/report");
1737
            if ($i !== false) {
1738
                $value = substr($value, 0, $i);
1739
            }
1740
            $value = $url1 . $value;
1741
        } elseif ($value === '@relation') {
1742
            if (isset($this->mfrelation[$curr_id]) && $curr_id != "") {
1743
                $value = (string) $this->mfrelation[$curr_id];
1744
            } else {
1745
                $value = "";
1746
            }
1747
        } elseif (preg_match("/@(\w+)/", $value, $match)) {
1748
            $gmatch = [];
1749
            if (preg_match("/\d $match[1] (.+)/", $this->gedrec, $gmatch)) {
1750
                $value = str_replace('@', '', trim($gmatch[1]));
1751
            }
1752
        } elseif (preg_match("/@\\$(\w+)/", $value, $match)) {
1753
            if ($match[1] == "dump" && $this->vars['dval']['id'] > 0) {
1754
                // if ($this->vars[ 'dval' ]['id'] == 1001)
1755
                if ($dumpvar == "gedrec") {
1756
                    error_log("\n---- setvar start  " . date("Y-m-d H:i:s") . " RPG " . __LINE__ . "  " . $name . "  gedcom=\n" . $this->gedrec . "\n", 3, "my-errors.log");
1757
                } elseif ($dumpvar != "") {
1758
                    error_log("var: " . $dumpvar . " = " . $this->vars[$dumpvar]['id'] . "\n", 3, "my-errors.log");
1759
                } else {
1760
                    if (array_key_exists('dval', $this->vars)) {
1761
                        $nnn = $this->vars['dval']['id'];
1762
                    } else {
1763
                        $nnn = 0;
1764
                    }
1765
                    error_log("\n---- setvar start  " . date("Y-m-d H:i:s") . " RPG " . __LINE__ . "  " . $name . "  -----\n", 3, "my-errors.log");
1766
                    foreach ($this->vars as $key => $val) {
1767
                        if ($nnn-- < 0) {
1768
                            error_log($key . "='" . $val['id'] . "'\n", 3, "my-errors.log");
1769
                        }
1770
                    }
1771
                }
1772
            }
1773
            $value = $this->vars[$match[1]]['id'];
1774
            if (isset($this->vars[$value]['id'])) {
1775
                $value = '$' . $this->vars[$match[1]]['id'];
1776
            } else {
1777
                $value = "0";
1778
            }
1779
        }
1780
        if (isset($attrs['trim'])) {
1781
            $value = str_replace($attrs['trim'], '', $value);
1782
        }
1783
        if (preg_match("/\\$(\w+)/", $name, $match)) {
1784
            $name = $this->vars["'" . $match[1] . "'"]['id'];
1785
        }
1786
        $count = preg_match_all("/\\$(\w+)/", $value, $match, PREG_SET_ORDER);
1787
        $i     = 0;
1788
        while ($i < $count) {
1789
            $t     = $this->vars[$match[$i][1]]['id'];
1790
            $value = preg_replace('/\$' . $match[$i][1] . '/', $t, $value, 1);
1791
            $i++;
1792
        }
1793
        if (preg_match('/^I18N::number\((.+)\)$/', $value, $match)) {
1794
            $value = I18N::number((int) $match[1]);
1795
        } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $value, $match)) {
1796
            $value = I18N::translate($match[1]);
1797
        } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $value, $match)) {
1798
            $value = I18N::translateContext($match[1], $match[2]);
1799
        }
1800
        if (isset($attrs['lcfirst'])) { // set 1st char to lower case
1801
            $value = lcfirst($value);
1802
        }
1803
1804
        // Arithmetic functions
1805
        if (preg_match("/(\d+)\s*([-+*\/])\s*(\d+)/", $value, $match)) {
1806
            // Create an expression language with the functions used by our reports.
1807
            $expression_provider  = new ReportExpressionLanguageProvider();
1808
            $expression_cache     = new NullAdapter();
1809
            $expression_language  = new ExpressionLanguage($expression_cache, [$expression_provider]);
1810
1811
            $value = (string) $expression_language->evaluate($value);
1812
        }
1813
1814
        if (str_contains($value, '@')) {
1815
            $value = '';
1816
        }
1817
        $this->vars[$name]['id'] = $value;
1818
        if ($name == 'title') {
1819
            $this->wt_report->title = $value;
1820
        }
1821
    }
1822
1823
    /**
1824
     * Handle <if>
1825
     *
1826
     * @param array<string> $attrs
1827
     *
1828
     * @return void
1829
     */
1830
    protected function ifStartHandler(array $attrs): void
1831
    {
1832
        if ($this->process_ifs > 0) {
1833
            $this->process_ifs++;
1834
1835
            return;
1836
        }
1837
1838
        $condition = $attrs['condition'];
1839
        $condition = $this->substituteVars($condition, true);
1840
        $condition = str_replace([
1841
            ' LT ',
1842
            ' GT ',
1843
        ], [
1844
            '<',
1845
            '>',
1846
        ], $condition);
1847
        // Replace the first occurrence only once of @fact:DATE or in any other combinations to the current fact, such as BIRT
1848
        $condition = str_replace('@fact:', $this->fact . ':', $condition);
1849
        $match     = [];
1850
        $count     = preg_match_all("/@([\w:.]+)/", $condition, $match, PREG_SET_ORDER);
1851
        $i         = 0;
1852
        while ($i < $count) {
1853
            $id    = $match[$i][1];
1854
            $value = '""';
1855
            if ($id === 'ID') {
1856
                if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1857
                    $value = "'" . $match[1] . "'";
1858
                }
1859
            } elseif ($id === 'fact') {
1860
                $value = '"' . $this->fact . '"';
1861
            } elseif ($id === 'desc') {
1862
                $value = '"' . addslashes($this->desc) . '"';
1863
            } elseif ($id === 'generation') {
1864
                $value = '"' . $this->generation . '"';
1865
            } else {
1866
                $level = (int) explode(' ', trim($this->gedrec))[0];
1867
                if ($level === 0) {
1868
                    $level++;
1869
                }
1870
                $value = $this->getGedcomValue($id, $level, $this->gedrec);
1871
                if (empty($value)) {
1872
                    $level++;
1873
                    $value = $this->getGedcomValue($id, $level, $this->gedrec);
1874
                }
1875
                $value = preg_replace('/^@(' . Gedcom::REGEX_XREF . ')@$/', '$1', $value);
1876
                $value = '"' . addslashes($value) . '"';
1877
            }
1878
            $condition = str_replace("@$id", $value, $condition);
1879
            $i++;
1880
        }
1881
1882
        // Create an expression language with the functions used by our reports.
1883
        $expression_provider  = new ReportExpressionLanguageProvider();
1884
        $expression_cache     = new NullAdapter();
1885
        $expression_language  = new ExpressionLanguage($expression_cache, [$expression_provider]);
1886
1887
        $ret = $expression_language->evaluate($condition);
1888
1889
        if (!$ret) {
1890
            $this->process_ifs++;
1891
        }
1892
    }
1893
1894
    /**
1895
     * Handle </if>
1896
     *
1897
     * @return void
1898
     */
1899
    protected function ifEndHandler(): void
1900
    {
1901
        if ($this->process_ifs > 0) {
1902
            $this->process_ifs--;
1903
        }
1904
    }
1905
1906
    /**
1907
     * Handle <footnote>
1908
     * Collect the Footnote links
1909
     * GEDCOM Records that are protected by Privacy setting will be ignored
1910
     *
1911
     * @param array<string> $attrs
1912
     *
1913
     * @return void
1914
     */
1915
    protected function footnoteStartHandler(array $attrs): void
1916
    {
1917
        $id = '';
1918
        if (preg_match('/[0-9] (.+) @(.+)@/', $this->gedrec, $match)) {
1919
            $id = $match[2];
1920
        }
1921
        $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1922
        if ($record && $record->canShow()) {
1923
            $this->print_data_stack[] = $this->print_data;
1924
            $this->print_data         = true;
1925
            $style                    = '';
1926
            if (!empty($attrs['style'])) {
1927
                $style = $attrs['style'];
1928
            }
1929
            $this->footnote_element = $this->current_element;
1930
            $this->current_element  = $this->report_root->createFootnote($style);
1931
        } else {
1932
            $this->print_data       = false;
1933
            $this->process_footnote = false;
1934
        }
1935
    }
1936
1937
    /**
1938
     * Handle </footnote>
1939
     * Print the collected Footnote data
1940
     *
1941
     * @return void
1942
     */
1943
    protected function footnoteEndHandler(): void
1944
    {
1945
        if ($this->process_footnote) {
1946
            $this->print_data = array_pop($this->print_data_stack);
1947
            $temp             = trim($this->current_element->getValue());
1948
            if (strlen($temp) > 3) {
1949
                $this->wt_report->addElement($this->current_element);
1950
            }
1951
            $this->current_element = $this->footnote_element;
1952
        } else {
1953
            $this->process_footnote = true;
1954
        }
1955
    }
1956
1957
    /**
1958
     * Handle <footnoteTexts />
1959
     *
1960
     * @return void
1961
     */
1962
    protected function footnoteTextsStartHandler(): void
1963
    {
1964
        $temp = 'footnotetexts';
1965
        $this->wt_report->addElement($temp);
1966
    }
1967
1968
    /**
1969
     * XML element Forced line break handler - HTML code
1970
     *
1971
     * @return void
1972
     */
1973
    protected function brStartHandler(): void
1974
    {
1975
        if ($this->print_data && $this->process_gedcoms === 0) {
1976
            $this->current_element->addText('<br>');
1977
        }
1978
    }
1979
1980
    /**
1981
     * Handle <sp />
1982
     * Forced space
1983
     *
1984
     * @return void
1985
     */
1986
    protected function spStartHandler(): void
1987
    {
1988
        if ($this->print_data && $this->process_gedcoms === 0) {
1989
            $this->current_element->addText(' ');
1990
        }
1991
    }
1992
1993
    /**
1994
     * Handle <highlightedImage />
1995
     *
1996
     * @param array<string> $attrs
1997
     *
1998
     * @return void
1999
     */
2000
    protected function highlightedImageStartHandler(array $attrs): void
2001
    {
2002
        $id = '';
2003
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
2004
            $id = $match[1];
2005
        }
2006
2007
        // Position the top corner of this box on the page
2008
        $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION);
2009
2010
        // Position the left corner of this box on the page
2011
        $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION);
2012
2013
        // string Align the image in left, center, right (or empty to use x/y position).
2014
        $align = $attrs['align'] ?? '';
2015
2016
        // string Next Line should be T:next to the image, N:next line
2017
        $ln = $attrs['ln'] ?? 'T';
2018
2019
        // Width, height (or both).
2020
        $width  = (float) ($attrs['width'] ?? 0.0);
2021
        $height = (float) ($attrs['height'] ?? 0.0);
2022
2023
        $person     = Registry::individualFactory()->make($id, $this->tree);
2024
        $media_file = $person->findHighlightedMediaFile();
2025
2026
        if ($media_file instanceof MediaFile && $media_file->fileExists()) {
2027
            $image      = imagecreatefromstring($media_file->fileContents());
2028
            $attributes = [imagesx($image), imagesy($image)];
2029
2030
            if ($width > 0 && $height == 0) {
2031
                $perc   = $width / $attributes[0];
2032
                $height = round($attributes[1] * $perc);
2033
            } elseif ($height > 0 && $width == 0) {
2034
                $perc  = $height / $attributes[1];
2035
                $width = round($attributes[0] * $perc);
2036
            } else {
2037
                $width  = (float) $attributes[0];
2038
                $height = (float) $attributes[1];
2039
            }
2040
            $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln);
2041
            $this->wt_report->addElement($image);
2042
        }
2043
    }
2044
2045
    /**
2046
     * Handle <image/>
2047
     *
2048
     * @param array<string> $attrs
2049
     *
2050
     * @return void
2051
     */
2052
    protected function imageStartHandler(array $attrs): void
2053
    {
2054
        // Position the top corner of this box on the page. the default is the current position
2055
        $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION);
2056
2057
        // mixed Position the left corner of this box on the page. the default is the current position
2058
        $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION);
2059
2060
        // string Align the image in left, center, right (or empty to use x/y position).
2061
        $align = $attrs['align'] ?? '';
2062
2063
        // string Next Line should be T:next to the image, N:next line
2064
        $ln = $attrs['ln'] ?? 'T';
2065
2066
        // Width, height (or both).
2067
        $width  = (float) ($attrs['width'] ?? 0.0);
2068
        $height = (float) ($attrs['height'] ?? 0.0);
2069
2070
        $file = $attrs['file'] ?? '';
2071
2072
        if ($file === '@FILE') {
2073
            $match = [];
2074
            if (preg_match("/\d OBJE @(.+)@/", $this->gedrec, $match)) {
2075
                $mediaobject = Registry::mediaFactory()->make($match[1], $this->tree);
2076
                $media_file  = $mediaobject->firstImageFile();
2077
2078
                if ($media_file instanceof MediaFile && $media_file->fileExists()) {
2079
                    $image      = imagecreatefromstring($media_file->fileContents());
2080
                    $attributes = [imagesx($image), imagesy($image)];
2081
2082
                    if ($width > 0 && $height == 0) {
2083
                        $perc   = $width / $attributes[0];
2084
                        $height = round($attributes[1] * $perc);
2085
                    } elseif ($height > 0 && $width == 0) {
2086
                        $perc  = $height / $attributes[1];
2087
                        $width = round($attributes[0] * $perc);
2088
                    } else {
2089
                        $width  = (float) $attributes[0];
2090
                        $height = (float) $attributes[1];
2091
                    }
2092
                    $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln);
2093
                    $this->wt_report->addElement($image);
2094
                }
2095
            }
2096
        } else {
2097
            if (file_exists($file) && preg_match('/(jpg|jpeg|png|gif)$/i', $file)) {
2098
                $size = getimagesize($file);
2099
                if ($width > 0 && $height == 0) {
2100
                    $perc   = $width / $size[0];
2101
                    $height = round($size[1] * $perc);
2102
                } elseif ($height > 0 && $width == 0) {
2103
                    $perc  = $height / $size[1];
2104
                    $width = round($size[0] * $perc);
2105
                } else {
2106
                    $width  = $size[0];
2107
                    $height = $size[1];
2108
                }
2109
                $image = $this->report_root->createImage($file, $left, $top, $width, $height, $align, $ln);
2110
                $this->wt_report->addElement($image);
2111
            }
2112
        }
2113
    }
2114
2115
    /**
2116
     * Handle <line>
2117
     *
2118
     * @param array<string> $attrs
2119
     *
2120
     * @return void
2121
     */
2122
    protected function lineStartHandler(array $attrs): void
2123
    {
2124
        // Start horizontal position, current position (default)
2125
        $x1 = ReportBaseElement::CURRENT_POSITION;
2126
        if (isset($attrs['x1'])) {
2127
            if ($attrs['x1'] === '0') {
2128
                $x1 = 0;
2129
            } elseif ($attrs['x1'] === '.') {
2130
                $x1 = ReportBaseElement::CURRENT_POSITION;
2131
            } elseif (!empty($attrs['x1'])) {
2132
                $x1 = (float) $attrs['x1'];
2133
            }
2134
        }
2135
        // Start vertical position, current position (default)
2136
        $y1 = ReportBaseElement::CURRENT_POSITION;
2137
        if (isset($attrs['y1'])) {
2138
            if ($attrs['y1'] === '0') {
2139
                $y1 = 0;
2140
            } elseif ($attrs['y1'] === '.') {
2141
                $y1 = ReportBaseElement::CURRENT_POSITION;
2142
            } elseif (!empty($attrs['y1'])) {
2143
                $y1 = (float) $attrs['y1'];
2144
            }
2145
        }
2146
        // End horizontal position, maximum width (default)
2147
        $x2 = ReportBaseElement::CURRENT_POSITION;
2148
        if (isset($attrs['x2'])) {
2149
            if ($attrs['x2'] === '0') {
2150
                $x2 = 0;
2151
            } elseif ($attrs['x2'] === '.') {
2152
                $x2 = ReportBaseElement::CURRENT_POSITION;
2153
            } elseif (!empty($attrs['x2'])) {
2154
                $x2 = (float) $attrs['x2'];
2155
            }
2156
        }
2157
        // End vertical position
2158
        $y2 = ReportBaseElement::CURRENT_POSITION;
2159
        if (isset($attrs['y2'])) {
2160
            if ($attrs['y2'] === '0') {
2161
                $y2 = 0;
2162
            } elseif ($attrs['y2'] === '.') {
2163
                $y2 = ReportBaseElement::CURRENT_POSITION;
2164
            } elseif (!empty($attrs['y2'])) {
2165
                $y2 = (float) $attrs['y2'];
2166
            }
2167
        }
2168
2169
        $line = $this->report_root->createLine($x1, $y1, $x2, $y2);
2170
        $this->wt_report->addElement($line);
2171
    }
2172
2173
    /**
2174
     * Handle <list>
2175
     *
2176
     * @param array<string> $attrs
2177
     *
2178
     * @return void
2179
     */
2180
    protected function listStartHandler(array $attrs): void
2181
    {
2182
        $this->process_repeats++;
2183
        if ($this->process_repeats > 1) {
2184
            return;
2185
        }
2186
2187
        $match = [];
2188
        if (isset($attrs['sortby'])) {
2189
            $sortby = $attrs['sortby'];
2190
            if (preg_match("/\\$(\w+)/", $sortby, $match)) {
2191
                $sortby = $this->vars[$match[1]]['id'];
2192
                $sortby = trim($sortby);
2193
            }
2194
        } else {
2195
            $sortby = 'NAME';
2196
        }
2197
2198
        $listname = $attrs['list'] ?? 'individual';
2199
2200
        // Some filters/sorts can be applied using SQL, while others require PHP
2201
        switch ($listname) {
2202
            case 'pending':
2203
                $this->list = DB::table('change')
2204
                    ->whereIn('change_id', function (Builder $query): void {
2205
                        $query->select([new Expression('MAX(change_id)')])
2206
                            ->from('change')
0 ignored issues
show
Bug introduced by
'change' of type string is incompatible with the type Closure|Illuminate\Datab...\Database\Query\Builder expected by parameter $table of Illuminate\Database\Query\Builder::from(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2206
                            ->from(/** @scrutinizer ignore-type */ 'change')
Loading history...
2207
                            ->where('gedcom_id', '=', $this->tree->id())
2208
                            ->where('status', '=', 'pending')
2209
                            ->groupBy(['xref']);
2210
                    })
2211
                    ->get()
2212
                    ->map(fn (object $row): ?GedcomRecord => Registry::gedcomRecordFactory()->make($row->xref, $this->tree, $row->new_gedcom ?: $row->old_gedcom))
2213
                    ->filter()
2214
                    ->all();
2215
                break;
2216
2217
            case 'individual':
2218
                $query = DB::table('individuals')
2219
                    ->where('i_file', '=', $this->tree->id())
2220
                    ->select(['i_id AS xref', 'i_gedcom AS gedcom'])
2221
                    ->distinct();
2222
2223
                foreach ($attrs as $attr => $value) {
2224
                    if (str_starts_with($attr, 'filter') && $value !== '') {
2225
                        $value = $this->substituteVars($value, false);
2226
                        // Convert the various filters into SQL
2227
                        if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
2228
                            $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2229
                                $join
2230
                                    ->on($attr . '.d_gid', '=', 'i_id')
2231
                                    ->on($attr . '.d_file', '=', 'i_file');
2232
                            });
2233
2234
                            $query->where($attr . '.d_fact', '=', $match[1]);
2235
2236
                            $date = new Date($match[3]);
2237
2238
                            if ($match[2] === 'LTE') {
2239
                                $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay());
2240
                            } else {
2241
                                $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay());
2242
                            }
2243
2244
                            // This filter has been fully processed
2245
                            unset($attrs[$attr]);
2246
                        } elseif (preg_match('/^NAME CONTAINS (.+)$/', $value, $match)) {
2247
                            $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2248
                                $join
2249
                                    ->on($attr . '.n_id', '=', 'i_id')
2250
                                    ->on($attr . '.n_file', '=', 'i_file');
2251
                            });
2252
                            // Search the DB only if there is any name supplied
2253
                            $names = explode(' ', $match[1]);
2254
                            foreach ($names as $name) {
2255
                                $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%');
2256
                            }
2257
2258
                            // This filter has been fully processed
2259
                            unset($attrs[$attr]);
2260
                        } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) {
2261
                            // Convert newline escape sequences to actual new lines
2262
                            $match[1] = str_replace('\n', "\n", $match[1]);
2263
2264
                            $query->where('i_gedcom', 'LIKE', $match[1]);
2265
2266
                            // This filter has been fully processed
2267
                            unset($attrs[$attr]);
2268
                        } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) {
2269
                            // Don't unset this filter. This is just initial filtering for performance
2270
                            $query
2271
                                ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void {
2272
                                    $join
2273
                                        ->on($attr . 'a.pl_file', '=', 'i_file')
2274
                                        ->on($attr . 'a.pl_gid', '=', 'i_id');
2275
                                })
2276
                                ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void {
2277
                                    $join
2278
                                        ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file')
2279
                                        ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id');
2280
                                })
2281
                                ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%');
2282
                        } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) {
2283
                            // Don't unset this filter. This is just initial filtering for performance
2284
                            $match[3] = strtr($match[3], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2285
                            $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%';
2286
                            $query->where('i_gedcom', 'LIKE', $like);
2287
                        } elseif (preg_match('/^(\w+) CONTAINS (.*)$/', $value, $match)) {
2288
                            // Don't unset this filter. This is just initial filtering for performance
2289
                            $match[2] = strtr($match[2], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2290
                            $like = "%\n1 " . $match[1] . '%' . $match[2] . '%';
2291
                            $query->where('i_gedcom', 'LIKE', $like);
2292
                        }
2293
                    }
2294
                }
2295
2296
                $this->list = [];
2297
2298
                foreach ($query->get() as $row) {
2299
                    $this->list[$row->xref] = Registry::individualFactory()->make($row->xref, $this->tree, $row->gedcom);
2300
                }
2301
                break;
2302
2303
            case 'family':
2304
                $query = DB::table('families')
2305
                    ->where('f_file', '=', $this->tree->id())
2306
                    ->select(['f_id AS xref', 'f_gedcom AS gedcom'])
2307
                    ->distinct();
2308
2309
                foreach ($attrs as $attr => $value) {
2310
                    if (str_starts_with($attr, 'filter') && $value !== '') {
2311
                        $value = $this->substituteVars($value, false);
2312
                        // Convert the various filters into SQL
2313
                        if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
2314
                            $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2315
                                $join
2316
                                    ->on($attr . '.d_gid', '=', 'f_id')
2317
                                    ->on($attr . '.d_file', '=', 'f_file');
2318
                            });
2319
2320
                            $query->where($attr . '.d_fact', '=', $match[1]);
2321
2322
                            $date = new Date($match[3]);
2323
2324
                            if ($match[2] === 'LTE') {
2325
                                $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay());
2326
                            } else {
2327
                                $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay());
2328
                            }
2329
2330
                            // This filter has been fully processed
2331
                            unset($attrs[$attr]);
2332
                        } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) {
2333
                            // Convert newline escape sequences to actual new lines
2334
                            $match[1] = str_replace('\n', "\n", $match[1]);
2335
2336
                            $query->where('f_gedcom', 'LIKE', $match[1]);
2337
2338
                            // This filter has been fully processed
2339
                            unset($attrs[$attr]);
2340
                        } elseif (preg_match('/^NAME CONTAINS (.*)$/', $value, $match)) {
2341
                            if ($sortby === 'NAME' || $match[1] !== '') {
2342
                                $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2343
                                    $join
2344
                                        ->on($attr . '.n_file', '=', 'f_file')
2345
                                        ->where(static function (Builder $query): void {
2346
                                            $query
2347
                                                ->whereColumn('n_id', '=', 'f_husb')
2348
                                                ->orWhereColumn('n_id', '=', 'f_wife');
2349
                                        });
2350
                                });
2351
                                // Search the DB only if there is any name supplied
2352
                                if ($match[1] != '') {
2353
                                    $names = explode(' ', $match[1]);
2354
                                    foreach ($names as $name) {
2355
                                        $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%');
2356
                                    }
2357
                                }
2358
                            }
2359
2360
                            // This filter has been fully processed
2361
                            unset($attrs[$attr]);
2362
                        } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) {
2363
                            // Don't unset this filter. This is just initial filtering for performance
2364
                            $query
2365
                                ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void {
2366
                                    $join
2367
                                        ->on($attr . 'a.pl_file', '=', 'f_file')
2368
                                        ->on($attr . 'a.pl_gid', '=', 'f_id');
2369
                                })
2370
                                ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void {
2371
                                    $join
2372
                                        ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file')
2373
                                        ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id');
2374
                                })
2375
                                ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%');
2376
                        } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) {
2377
                            // Don't unset this filter. This is just initial filtering for performance
2378
                            $match[3] = strtr($match[3], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2379
                            $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%';
2380
                            $query->where('f_gedcom', 'LIKE', $like);
2381
                        } elseif (preg_match('/^(\w+) CONTAINS (.+)$/', $value, $match)) {
2382
                            // Don't unset this filter. This is just initial filtering for performance
2383
                            $match[2] = strtr($match[2], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2384
                            $like = "%\n1 " . $match[1] . '%' . $match[2] . '%';
2385
                            $query->where('f_gedcom', 'LIKE', $like);
2386
                        }
2387
                    }
2388
                }
2389
2390
                $this->list = [];
2391
2392
                foreach ($query->get() as $row) {
2393
                    $this->list[$row->xref] = Registry::familyFactory()->make($row->xref, $this->tree, $row->gedcom);
2394
                }
2395
                break;
2396
2397
            default:
2398
                throw new DomainException('Invalid list name: ' . $listname);
2399
        }
2400
2401
        $filters  = [];
2402
        $filters2 = [];
2403
        if (isset($attrs['filter1']) && count($this->list) > 0) {
2404
            foreach ($attrs as $key => $value) {
2405
                if (preg_match("/filter(\d)/", $key)) {
2406
                    $condition = $value;
2407
                    if (preg_match("/@(\w+)/", $condition, $match)) {
2408
                        $id    = $match[1];
2409
                        $value = "''";
2410
                        if ($id === 'ID') {
2411
                            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
2412
                                $value = "'" . $match[1] . "'";
2413
                            }
2414
                        } elseif ($id === 'fact') {
2415
                            $value = "'" . $this->fact . "'";
2416
                        } elseif ($id === 'desc') {
2417
                            $value = "'" . $this->desc . "'";
2418
                        } else {
2419
                            if (preg_match("/\d $id (.+)/", $this->gedrec, $match)) {
2420
                                $value = "'" . str_replace('@', '', trim($match[1])) . "'";
2421
                            }
2422
                        }
2423
                        $condition = preg_replace("/@$id/", $value, $condition);
2424
                    }
2425
                    //-- handle regular expressions
2426
                    if (preg_match("/([A-Z:]+)\s*([^\s]+)\s*(.+)/", $condition, $match)) {
2427
                        $tag  = trim($match[1]);
2428
                        $expr = trim($match[2]);
2429
                        $val  = trim($match[3]);
2430
                        if (preg_match("/\\$(\w+)/", $val, $match)) {
2431
                            $val = $this->vars[$match[1]]['id'];
2432
                            $val = trim($val);
2433
                        }
2434
                        if ($val !== '') {
2435
                            $searchstr = '';
2436
                            $tags      = explode(':', $tag);
2437
                            //-- only limit to a level number if we are specifically looking at a level
2438
                            if (count($tags) > 1) {
2439
                                $level = 1;
2440
                                $t = 'XXXX';
2441
                                foreach ($tags as $t) {
2442
                                    if (!empty($searchstr)) {
2443
                                        $searchstr .= "[^\n]*(\n[2-9][^\n]*)*\n";
2444
                                    }
2445
                                    //-- search for both EMAIL and _EMAIL... silly double gedcom standard
2446
                                    if ($t === 'EMAIL' || $t === '_EMAIL') {
2447
                                        $t = '_?EMAIL';
2448
                                    }
2449
                                    $searchstr .= $level . ' ' . $t;
2450
                                    $level++;
2451
                                }
2452
                            } else {
2453
                                if ($tag === 'EMAIL' || $tag === '_EMAIL') {
2454
                                    $tag = '_?EMAIL';
2455
                                }
2456
                                $t         = $tag;
2457
                                $searchstr = '1 ' . $tag;
2458
                            }
2459
                            switch ($expr) {
2460
                                case 'CONTAINS':
2461
                                    if ($t === 'PLAC') {
2462
                                        $searchstr .= "[^\n]*[, ]*" . $val;
2463
                                    } else {
2464
                                        $searchstr .= "[^\n]*" . $val;
2465
                                    }
2466
                                    $filters[] = $searchstr;
2467
                                    break;
2468
                                default:
2469
                                    $filters2[] = [
2470
                                        'tag'  => $tag,
2471
                                        'expr' => $expr,
2472
                                        'val'  => $val,
2473
                                    ];
2474
                                    break;
2475
                            }
2476
                        }
2477
                    }
2478
                }
2479
            }
2480
        }
2481
        //-- apply other filters to the list that could not be added to the search string
2482
        if ($filters !== []) {
2483
            foreach ($this->list as $key => $record) {
2484
                foreach ($filters as $filter) {
2485
                    if (!preg_match('/' . $filter . '/i', $record->privatizeGedcom(Auth::accessLevel($this->tree)))) {
2486
                        unset($this->list[$key]);
2487
                        break;
2488
                    }
2489
                }
2490
            }
2491
        }
2492
        if ($filters2 !== []) {
2493
            $mylist = [];
2494
            foreach ($this->list as $indi) {
2495
                $key  = $indi->xref();
2496
                $grec = $indi->privatizeGedcom(Auth::accessLevel($this->tree));
2497
                $keep = true;
2498
                foreach ($filters2 as $filter) {
2499
                    if ($keep) {
2500
                        $tag  = $filter['tag'];
2501
                        $expr = $filter['expr'];
2502
                        $val  = $filter['val'];
2503
                        if ($val === "''") {
2504
                            $val = '';
2505
                        }
2506
                        $tags = explode(':', $tag);
2507
                        $t    = end($tags);
2508
                        $v    = $this->getGedcomValue($tag, 1, $grec);
2509
                        //-- check for EMAIL and _EMAIL (silly double gedcom standard :P)
2510
                        if ($t === 'EMAIL' && empty($v)) {
2511
                            $tag  = str_replace('EMAIL', '_EMAIL', $tag);
2512
                            $tags = explode(':', $tag);
2513
                            $t    = end($tags);
2514
                            $v    = self::getSubRecord(1, $tag, $grec);
2515
                        }
2516
2517
                        switch ($expr) {
2518
                            case 'GTE':
2519
                                if ($t === 'DATE') {
2520
                                    $date1 = new Date($v);
2521
                                    $date2 = new Date($val);
2522
                                    $keep  = (Date::compare($date1, $date2) >= 0);
2523
                                } elseif ($val >= $v) {
2524
                                    $keep = true;
2525
                                }
2526
                                break;
2527
                            case 'LTE':
2528
                                if ($t === 'DATE') {
2529
                                    $date1 = new Date($v);
2530
                                    $date2 = new Date($val);
2531
                                    $keep  = (Date::compare($date1, $date2) <= 0);
2532
                                } elseif ($val >= $v) {
2533
                                    $keep = true;
2534
                                }
2535
                                break;
2536
                            default:
2537
                                if ($v == $val) {
2538
                                    $keep = true;
2539
                                } else {
2540
                                    $keep = false;
2541
                                }
2542
                                break;
2543
                        }
2544
                    }
2545
                }
2546
                if ($keep) {
2547
                    $mylist[$key] = $indi;
2548
                }
2549
            }
2550
            $this->list = $mylist;
2551
        }
2552
2553
        switch ($sortby) {
2554
            case 'NAME':
2555
                uasort($this->list, GedcomRecord::nameComparator());
2556
                break;
2557
            case 'CHAN':
2558
                uasort($this->list, GedcomRecord::lastChangeComparator());
2559
                break;
2560
            case 'BIRT:DATE':
2561
                uasort($this->list, Individual::birthDateComparator());
2562
                break;
2563
            case 'DEAT:DATE':
2564
                uasort($this->list, Individual::deathDateComparator());
2565
                break;
2566
            case 'MARR:DATE':
2567
                uasort($this->list, Family::marriageDateComparator());
2568
                break;
2569
            default:
2570
                // unsorted or already sorted by SQL
2571
                break;
2572
        }
2573
2574
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
2575
        $this->repeat_bytes    = xml_get_current_line_number($this->parser) + 1;
2576
    }
2577
2578
    /**
2579
     * Handle </list>
2580
     *
2581
     * @return void
2582
     */
2583
    protected function listEndHandler(): void
2584
    {
2585
        $this->process_repeats--;
2586
        if ($this->process_repeats > 0) {
2587
            return;
2588
        }
2589
2590
        // Check if there is any list
2591
        if (count($this->list) > 0) {
2592
            $lineoffset = 0;
2593
            foreach ($this->repeats_stack as $rep) {
2594
                $lineoffset = $lineoffset + (int) ($rep[1]) - 1;
2595
            }
2596
            //-- read the xml from the file
2597
            $lines = file($this->report);
2598
            if (empty($lines)) {
2599
                error_log(__FILE__ . ":" . __LINE__ . " impossible error!? \n");
2600
                // this can not happen! phpstan forces me to add stupid code
2601
                // we come here because the xml file exists! Is it possible to tell phpstan the $lines *is* an array ??
2602
                die("can not happen!!!");
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
2603
            }
2604
            while ((!str_contains($lines[$lineoffset + $this->repeat_bytes], '<List')) && (($lineoffset + $this->repeat_bytes) > 0)) {
2605
                $lineoffset--;
2606
            }
2607
            $lineoffset++;
2608
            $reportxml = "<tempdoc>\n";
2609
            $line_nr   = $lineoffset + $this->repeat_bytes;
2610
            // List Level counter
2611
            $count = 1;
2612
            while (0 < $count) {
2613
                if (str_contains($lines[$line_nr], '<List')) {
2614
                    $count++;
2615
                } elseif (str_contains($lines[$line_nr], '</List')) {
2616
                    $count--;
2617
                }
2618
                if (0 < $count) {
2619
                    $reportxml .= $lines[$line_nr];
2620
                }
2621
                $line_nr++;
2622
            }
2623
            // No need to drag this
2624
            unset($lines);
2625
            $reportxml .= '</tempdoc>';
2626
            // Save original values
2627
            $this->parser_stack[] = $this->parser;
2628
            $oldgedrec            = $this->gedrec;
2629
2630
            $this->list_total   = count($this->list);
2631
            $this->list_private = 0;
2632
            foreach ($this->list as $record) {
2633
                if ($record->canShow()) {
2634
                    $this->gedrec = $record->privatizeGedcom(Auth::accessLevel($record->tree()));
2635
                    //-- start the sax parser
2636
                    $repeat_parser = xml_parser_create();
2637
                    $this->parser  = $repeat_parser;
2638
                    xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
2639
2640
                    xml_set_element_handler(
2641
                        $repeat_parser,
2642
                        function ($parser, string $name, array $attrs): void {
2643
                            $this->startElement($parser, $name, $attrs);
2644
                        },
2645
                        function ($parser, string $name): void {
2646
                            $this->endElement($parser, $name);
2647
                        }
2648
                    );
2649
2650
                    xml_set_character_data_handler(
2651
                        $repeat_parser,
2652
                        function ($parser, string $data): void {
2653
                            $this->characterData($parser, $data);
2654
                        }
2655
                    );
2656
2657
                    if (!xml_parse($repeat_parser, $reportxml, true)) {
2658
                        throw new DomainException(sprintf(
2659
                            'ListEHandler XML error: %s at line %d',
2660
                            xml_error_string(xml_get_error_code($repeat_parser)),
2661
                            xml_get_current_line_number($repeat_parser)
2662
                        ));
2663
                    }
2664
                    xml_parser_free($repeat_parser);
2665
                } else {
2666
                    $this->list_private++;
2667
                }
2668
            }
2669
            $this->list   = [];
2670
            $this->parser = array_pop($this->parser_stack);
2671
            $this->gedrec = $oldgedrec;
2672
        }
2673
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
2674
    }
2675
2676
    /**
2677
     * Handle <listTotal>
2678
     * Prints the total number of records in a list
2679
     * The total number is collected from <list> and <relatives>
2680
     *
2681
     * @return void
2682
     */
2683
    protected function listTotalStartHandler(): void
2684
    {
2685
        if ($this->list_private == 0) {
2686
            $this->current_element->addText((string) $this->list_total);
2687
        } else {
2688
            $this->current_element->addText(($this->list_total - $this->list_private) . ' / ' . $this->list_total);
2689
        }
2690
    }
2691
2692
    /**
2693
     * Handle <relatives>
2694
     *
2695
     * @param array<string> $attrs
2696
     *
2697
     * @return void
2698
     */
2699
    protected function relativesStartHandler(array $attrs): void
2700
    {
2701
        $this->process_repeats++;
2702
        if ($this->process_repeats > 1) {
2703
            return;
2704
        }
2705
2706
        $sortby = $attrs['sortby'] ?? 'NAME';
2707
2708
        $match = [];
2709
        if (preg_match("/\\$(\w+)/", $sortby, $match)) {
2710
            $sortby = $this->vars[$match[1]]['id'];
2711
            $sortby = trim($sortby);
2712
        }
2713
2714
        $maxgen = -1;
2715
        if (isset($attrs['maxgen'])) {
2716
            $maxgen = (int) $attrs['maxgen'];
2717
        }
2718
2719
        $group = $attrs['group'] ?? 'child-family';
2720
2721
        if (preg_match("/\\$(\w+)/", $group, $match)) {
2722
            $group = $this->vars[$match[1]]['id'];
2723
            $group = trim($group);
2724
        }
2725
2726
        $id = $attrs['id'] ?? '';
2727
2728
        if (preg_match("/\\$(\w+)/", $id, $match)) {
2729
            $id = $this->vars[$match[1]]['id'];
2730
            $id = trim($id);
2731
        }
2732
2733
        $this->list = [];
2734
        $person     = Registry::individualFactory()->make($id, $this->tree);
2735
        if ($person instanceof Individual) {
2736
            $this->list[$id] = $person;
2737
            $this->mfrelation[$id] = "";
2738
            $nam = $person->getAllNames()[0]['fullNN'];
0 ignored issues
show
Unused Code introduced by
The assignment to $nam is dead and can be removed.
Loading history...
2739
            switch ($group) {
2740
                case 'child-family':
2741
                    foreach ($person->childFamilies() as $family) {
2742
                        foreach ($family->spouses() as $spouse) {
2743
                            $this->list[$spouse->xref()] = $spouse;
2744
                        }
2745
2746
                        foreach ($family->children() as $child) {
2747
                            $this->list[$child->xref()] = $child;
2748
                        }
2749
                    }
2750
                    break;
2751
                case 'spouse-family':
2752
                    foreach ($person->spouseFamilies() as $family) {
2753
                        foreach ($family->spouses() as $spouse) {
2754
                            $this->list[$spouse->xref()] = $spouse;
2755
                        }
2756
2757
                        foreach ($family->children() as $child) {
2758
                            $this->list[$child->xref()] = $child;
2759
                        }
2760
                    }
2761
                    break;
2762
                case 'direct-ancestors':
2763
                    $this->addAncestors($this->list, $id, false, $maxgen);
2764
                    break;
2765
                case 'ancestors':
2766
                    $this->addAncestors($this->list, $id, true, $maxgen);
2767
                    break;
2768
                case 'descendants':
2769
                    $this->list[$id]->generation = 1;
2770
                    $this->addDescendancy($this->list, $id, false, $maxgen);
2771
                    break;
2772
                case 'all':
2773
                    $this->addAncestors($this->list, $id, true, $maxgen);
2774
                    $this->addDescendancy($this->list, $id, true, $maxgen);
2775
                    break;
2776
            }
2777
        }
2778
2779
        switch ($sortby) {
2780
            case 'NAME':
2781
                uasort($this->list, GedcomRecord::nameComparator());
2782
                break;
2783
            case 'BIRT:DATE':
2784
                uasort($this->list, Individual::birthDateComparator());
2785
                break;
2786
            case 'DEAT:DATE':
2787
                uasort($this->list, Individual::deathDateComparator());
2788
                break;
2789
            case 'generation':
2790
                $newarray = [];
2791
                reset($this->list);
2792
                $genCounter = 1;
2793
                while (count($newarray) < count($this->list)) {
2794
                    foreach ($this->list as $key => $value) {
2795
                        if ($value->generation < 0) {
2796
                            // indication of husband or wife
2797
                            $this->generation = -$value->generation;
2798
                        } else {
2799
                            $this->generation = $value->generation;
2800
                        }
2801
                        if ($this->generation == $genCounter) {
2802
                            $newarray[$key] = (object) ['generation' => $this->generation];
2803
                        }
2804
                    }
2805
                    $genCounter++;
2806
                }
2807
                $this->list = $newarray;
2808
                break;
2809
            default:
2810
                // unsorted
2811
                break;
2812
        }
2813
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
2814
        $this->repeat_bytes    = xml_get_current_line_number($this->parser) + 1;
2815
    }
2816
2817
    /**
2818
     * Handle </relatives>
2819
     *
2820
     * @return void
2821
     */
2822
    protected function relativesEndHandler(): void
2823
    {
2824
        $this->process_repeats--;
2825
        if ($this->process_repeats > 0) {
2826
            return;
2827
        }
2828
2829
        // Check if there is any relatives
2830
        if (count($this->list) > 0) {
2831
            $lineoffset = 0;
2832
            foreach ($this->repeats_stack as $rep) {
2833
                $lineoffset = $lineoffset + (int) ($rep[1]) - 1;
2834
            }
2835
            //-- read the xml from the file
2836
            $lines = file($this->report);
2837
            if (empty($lines)) {
2838
                error_log(__FILE__ . ":" . __LINE__ . " impossible error!? \n");
2839
                // this can not happen! phpstan forces me to add stupid code
2840
                // we come here because the xml file exists! Is it possible to tell phpstan the $lines *is* an array ??
2841
                die("can not happen!!!");
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
2842
            }
2843
            while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<Relatives') && $lineoffset + $this->repeat_bytes > 0) {
2844
                $lineoffset--;
2845
            }
2846
            $lineoffset++;
2847
            $reportxml = "<tempdoc>\n";
2848
            $line_nr   = $lineoffset + $this->repeat_bytes;
2849
            // Relatives Level counter
2850
            $count = 1;
2851
            while (0 < $count) {
2852
                if (str_contains($lines[$line_nr], '<Relatives')) {
2853
                    $count++;
2854
                } elseif (str_contains($lines[$line_nr], '</Relatives')) {
2855
                    $count--;
2856
                }
2857
                if (0 < $count) {
2858
                    $reportxml .= $lines[$line_nr];
2859
                }
2860
                $line_nr++;
2861
            }
2862
            // No need to drag this
2863
            unset($lines);
2864
            $reportxml .= "</tempdoc>\n";
2865
            // Save original values
2866
            $this->parser_stack[] = $this->parser;
2867
            $oldgedrec            = $this->gedrec;
2868
2869
            $this->list_total   = count($this->list);
2870
            $this->list_private = 0;
2871
            foreach ($this->list as $key => $value) {
2872
                if (isset($value->generation)) {
2873
                    $this->generation = $value->generation;
2874
                }
2875
                $xref = $key;
2876
                $this->vars["dupl"]["id"] = "no";
2877
                if (substr($key, 0, 2) == "D_") {
2878
                    $xref = substr($key, strrpos($key, "_") + 1);
2879
                    $this->vars["dupl"]["id"] = "yes";
2880
                }
2881
                $tmp          = Registry::gedcomRecordFactory()->make((string) $xref, $this->tree);
2882
                $this->gedrec = $tmp->privatizeGedcom(Auth::accessLevel($this->tree));
2883
2884
                $repeat_parser = xml_parser_create();
2885
                $this->parser  = $repeat_parser;
2886
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
2887
2888
                xml_set_element_handler(
2889
                    $repeat_parser,
2890
                    function ($parser, string $name, array $attrs): void {
2891
                        $this->startElement($parser, $name, $attrs);
2892
                    },
2893
                    function ($parser, string $name): void {
2894
                        $this->endElement($parser, $name);
2895
                    }
2896
                );
2897
2898
                xml_set_character_data_handler(
2899
                    $repeat_parser,
2900
                    function ($parser, string $data): void {
2901
                        $this->characterData($parser, $data);
2902
                    }
2903
                );
2904
2905
                if (!xml_parse($repeat_parser, $reportxml, true)) {
2906
                    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)));
2907
                }
2908
                xml_parser_free($repeat_parser);
2909
            }
2910
            // Clean up the list array
2911
            $this->list   = [];
2912
            $this->parser = array_pop($this->parser_stack);
2913
            $this->gedrec = $oldgedrec;
2914
        }
2915
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
2916
    }
2917
2918
    /**
2919
     * Handle <generation />
2920
     * Prints the number of generations
2921
     *
2922
     * @return void
2923
     */
2924
    protected function generationStartHandler(): void
2925
    {
2926
        $this->current_element->addText((string) $this->generation);
2927
    }
2928
2929
    /**
2930
     * Handle <newPage />
2931
     * Has to be placed in an element (header, body or footer)
2932
     *
2933
     * @return void
2934
     */
2935
    protected function newPageStartHandler(): void
2936
    {
2937
        $temp = 'addpage';
2938
        $this->wt_report->addElement($temp);
2939
    }
2940
2941
    /**
2942
     * Handle </title>
2943
     *
2944
     * @return void
2945
     */
2946
    protected function titleEndHandler(): void
2947
    {
2948
        $this->report_root->addTitle($this->text);
2949
    }
2950
2951
    /**
2952
     * Handle </description>
2953
     *
2954
     * @return void
2955
     */
2956
    protected function descriptionEndHandler(): void
2957
    {
2958
        $this->report_root->addDescription($this->text);
2959
    }
2960
2961
    /**
2962
     * Create a list of all descendants.
2963
     *
2964
     * @param array<Individual> $list
2965
     * @param string            $pid
2966
     * @param bool              $parents
2967
     * @param int               $generations
2968
     *
2969
     * @return void
2970
     */
2971
    private function addDescendancy(&$list, $pid, $parents = false, $generations = -1): void
2972
    {
2973
        $person = Registry::individualFactory()->make($pid, $this->tree);
2974
        if ($person === null) {
2975
            return;
2976
        }
2977
2978
        static $focusperson = true;
2979
        static $dupl = 1;
2980
        $sx = $person->sex();
2981
        $rl = "x"; // unknown
2982
        if ($sx == "M") {
2983
            $rl = "s";
2984
        } // son
2985
        if ($sx == "F") {
2986
            $rl = "d";
0 ignored issues
show
Unused Code introduced by
The assignment to $rl is dead and can be removed.
Loading history...
2987
        } // daughter
2988
        if ($focusperson) {
2989
            $this->mfrelation[$pid] = "";
2990
        }
2991
        $nam = $person->getAllNames()[0]['fullNN'];
0 ignored issues
show
Unused Code introduced by
The assignment to $nam is dead and can be removed.
Loading history...
2992
2993
        $newpid = $pid;
2994
        if (!isset($list[$pid])) {
2995
            $list[$pid] = $person;
2996
        } elseif (!$focusperson) {
2997
            $newpid = "D_" . $dupl . "_" . $pid;
2998
            $list[$newpid] = $person;
2999
        }
3000
        if (!isset($list[$newpid]->generation)) {
3001
            $list[$newpid]->generation = 0;
3002
        }
3003
        $focusperson = false;
3004
        foreach ($person->spouseFamilies() as $family) {
3005
            if ($parents) {
3006
                $husband = $family->husband();
3007
                $wife    = $family->wife();
3008
                if ($husband) {
3009
                    $list[$husband->xref()] = $husband;
3010
                    if (isset($list[$pid]->generation)) {
3011
                        $list[$husband->xref()]->generation = $list[$pid]->generation - 1;
3012
                    } else {
3013
                        $list[$husband->xref()]->generation = 1;
3014
                    }
3015
                }
3016
                if ($wife) {
3017
                    $list[$wife->xref()] = $wife;
3018
                    if (isset($list[$pid]->generation)) {
3019
                        $list[$wife->xref()]->generation = $list[$pid]->generation - 1;
3020
                    } else {
3021
                        $list[$wife->xref()]->generation = 1;
3022
                    }
3023
                }
3024
            }
3025
            $husband = $family->husband();
3026
            $wife = $family->wife();
3027
3028
            if ($husband && $wife) {
3029
                if ($husband->xref() == $person->xref()) {
3030
                    $this->mfrelation[$wife->xref()] = $this->mfrelation[$person->xref()] . "x";
3031
                    if ($wife->canShow()) {
3032
                        $list[$wife->xref()] = $wife;
3033
                    }
3034
                    if (!isset($wife->generation)) {
3035
                        $wife->generation = $person->generation;
3036
                    }
3037
                    $nam = $wife->getAllNames()[0]['fullNN'];
3038
                } else {
3039
                    $this->mfrelation[$husband->xref()] = $this->mfrelation[$person->xref()] . "x";
3040
                    if ($husband->canShow()) {
3041
                        $list[$husband->xref()] = $husband;
3042
                    }
3043
                    if (!isset($husband->generation)) {
3044
                        $husband->generation = $person->generation;
3045
                    }
3046
                    $nam = $husband->getAllNames()[0]['fullNN'];
3047
                }
3048
            }
3049
3050
            $children = $family->children();
3051
            foreach ($children as $child) {
3052
                if ($child) {
3053
                    $sx = $child->sex();
3054
                    $rl = "x"; // unknown
3055
                    if ($sx == "M") {
3056
                        $rl = "s";
3057
                    } // son
3058
                    if ($sx == "F") {
3059
                        $rl = "d";
3060
                    } // daughter
3061
                    $rl = $this->mfrelation[$person->xref()] . $rl;
3062
                    $this->mfrelation[$child->xref()] = $rl;
3063
                    if (isset($list[$pid]->generation)) {
3064
                        $child->generation = $list[$pid]->generation + 1;
3065
                    } else {
3066
                        $child->generation = 2;
3067
                    }
3068
                }
3069
            }
3070
            if ($generations == -1 || $list[$pid]->generation < $generations) {
3071
                foreach ($children as $child) {
3072
                    if ($child->canShow()) {
3073
                        $this->addDescendancy($list, $child->xref(), $parents, $generations);
3074
                    } // recurse on the childs family
3075
                }
3076
            }
3077
        }
3078
        $focusperson = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $focusperson is dead and can be removed.
Loading history...
3079
    }
3080
3081
    /**
3082
     * Create a list of all ancestors.
3083
     *
3084
     * @param array<Individual> $list
3085
     * @param string            $pid
3086
     * @param bool              $children
3087
     * @param int               $generations
3088
     *
3089
     * @return void
3090
     */
3091
    private function addAncestors(array &$list, string $pid, bool $children = false, int $generations = -1): void
3092
    {
3093
        $genlist                = [$pid];
3094
        $list[$pid]->generation = 1;
3095
        while (count($genlist) > 0) {
3096
            $id = array_shift($genlist);
3097
            if (str_starts_with($id, 'empty')) {
3098
                continue; // id can be something like “empty7”
3099
            }
3100
            if (!isset($this->mfrelation[$id])) {
3101
                $this->mfrelation[$id] = "";
3102
            }
3103
            $person = Registry::individualFactory()->make($id, $this->tree);
3104
            foreach ($person->childFamilies() as $family) {
3105
                $husband = $family->husband();
3106
                $wife    = $family->wife();
3107
                if ($husband) {
3108
                    $list[$husband->xref()]             = $husband;
3109
                    $list[$husband->xref()]->generation = $list[$id]->generation + 1;
3110
                    $this->mfrelation[$husband->xref()] = $this->mfrelation[$id] . "f";
3111
                }
3112
                if ($wife) {
3113
                    $list[$wife->xref()]             = $wife;
3114
                    $list[$wife->xref()]->generation = $list[$id]->generation + 1;
3115
                    $this->mfrelation[$wife->xref()] = $this->mfrelation[$id] . "m";
3116
                }
3117
                if ($generations == -1 || $list[$id]->generation + 1 < $generations) {
3118
                    if ($husband) {
3119
                        $genlist[] = $husband->xref();
3120
                    }
3121
                    if ($wife) {
3122
                        $genlist[] = $wife->xref();
3123
                    }
3124
                }
3125
                if ($children && isset($person)) {
3126
                    // unnecessary test of $person to satisfy phpstan!
3127
                    foreach ($family->children() as $child) {
3128
                        $list[$child->xref()] = $child;
3129
                        $child->generation = $list[$id]->generation ?? 1;
3130
                        if ($child->xref() != $person->xref()) {
3131
                            $this->mfrelation[$child->xref()] = $this->mfrelation[$id] . "x";
3132
                        }
3133
                    }
3134
                }
3135
            }
3136
        }
3137
    }
3138
3139
    /**
3140
     * get gedcom tag value
3141
     *
3142
     * @param string $tag    The tag to find, use : to delineate subtags
3143
     * @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
3144
     * @param string $gedrec The gedcom record to get the value from
3145
     *
3146
     * @return string the value of a gedcom tag from the given gedcom record
3147
     */
3148
    private function getGedcomValue(string $tag, int $level, string $gedrec): string
3149
    {
3150
        if ($gedrec === '') {
3151
            return '';
3152
        }
3153
        $tags      = explode(':', $tag);
3154
        $origlevel = $level;
3155
        if ($level === 0) {
3156
            $level = 1 + (int) $gedrec[0];
3157
        }
3158
3159
        $subrec = $gedrec;
3160
        $t = 'XXXX';
3161
        foreach ($tags as $t) {
3162
            $lastsubrec = $subrec;
3163
            $subrec     = self::getSubRecord($level, "$level $t", $subrec);
3164
            if (empty($subrec) && $origlevel == 0) {
3165
                $level--;
3166
                $subrec = self::getSubRecord($level, "$level $t", $lastsubrec);
3167
            }
3168
            if (empty($subrec)) {
3169
                if ($t === 'TITL') {
3170
                    $subrec = self::getSubRecord($level, "$level ABBR", $lastsubrec);
3171
                    if (!empty($subrec)) {
3172
                        $t = 'ABBR';
3173
                    }
3174
                }
3175
                if ($subrec === '') {
3176
                    if ($level > 0) {
3177
                        $level--;
3178
                    }
3179
                    $subrec = self::getSubRecord($level, "@ $t", $gedrec);
3180
                    if ($subrec === '') {
3181
                        return '';
3182
                    }
3183
                }
3184
            }
3185
            $level++;
3186
        }
3187
        $level--;
3188
        $ct = preg_match("/$level $t(.*)/", $subrec, $match);
3189
        if ($ct === 0) {
3190
            $ct = preg_match("/$level @.+@ (.+)/", $subrec, $match);
3191
        }
3192
        if ($ct === 0) {
3193
            $ct = preg_match("/@ $t (.+)/", $subrec, $match);
3194
        }
3195
        if ($ct > 0) {
3196
            $value = trim($match[1]);
3197
            if ($t === 'NOTE' && preg_match('/^@(.+)@$/', $value, $match)) {
3198
                $note = Registry::noteFactory()->make($match[1], $this->tree);
3199
                if ($note instanceof Note) {
3200
                    $value = $note->getNote();
3201
                } else {
3202
                    //-- set the value to the id without the @
3203
                    $value = $match[1];
3204
                }
3205
            }
3206
            if ($level !== 0 || $t !== 'NOTE') {
3207
                $value .= self::getCont($level + 1, $subrec);
3208
            }
3209
3210
            if ($tag === 'NAME' || $tag === '_MARNM' || $tag === '_AKA') {
3211
                return strtr($value, ['/' => '']);
3212
            }
3213
3214
            if ($tag === 'NAME' || $tag === '_MARNM' || $tag === '_AKA') {
3215
                return strtr($value, ['/' => '']);
3216
            }
3217
3218
            return $value;
3219
        }
3220
3221
        return '';
3222
    }
3223
3224
    /**
3225
     * Replace variable identifiers with their values.
3226
     *
3227
     * @param string $expression An expression such as "$foo == 123"
3228
     * @param bool   $quote      Whether to add quotation marks
3229
     *
3230
     * @return string
3231
     */
3232
    private function substituteVars($expression, $quote): string
3233
    {
3234
        return preg_replace_callback(
3235
            '/\$(\w+)/',
3236
            function (array $matches) use ($quote): string {
3237
                if (isset($this->vars[$matches[1]]['id'])) {
3238
                    if ($quote) {
3239
                        return "'" . addcslashes($this->vars[$matches[1]]['id'], "'") . "'";
3240
                    }
3241
3242
                    return $this->vars[$matches[1]]['id'];
3243
                }
3244
3245
                Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1]));
3246
3247
                return '$' . $matches[1];
3248
            },
3249
            $expression
3250
        );
3251
    }
3252
}
3253