Passed
Pull Request — main (#4945)
by
unknown
06:14
created

ReportParserGenerate   F

Complexity

Total Complexity 631

Size/Duplication

Total Lines 3135
Duplicated Lines 0 %

Importance

Changes 19
Bugs 0 Features 0
Metric Value
eloc 1724
c 19
b 0
f 0
dl 0
loc 3135
rs 0.8
wmc 631

54 Methods

Rating   Name   Duplication   Size   Complexity  
A pageNumStartHandler() 0 3 1
C gedcomStartHandler() 0 55 14
A textEndHandler() 0 4 1
F textBoxStartHandler() 0 139 36
A cellEndHandler() 0 4 1
A nowStartHandler() 0 3 1
A gedcomEndHandler() 0 6 2
A totalPagesStartHandler() 0 3 1
A textBoxEndHandler() 0 11 1
A textStartHandler() 0 18 3
A headerStartHandler() 0 5 1
A footerStartHandler() 0 3 1
B getSubRecord() 0 30 7
C endElement() 0 7 13
A styleStartHandler() 0 14 2
A bodyStartHandler() 0 3 1
F cellStartHandler() 0 118 21
F docStartHandler() 0 85 28
A docEndHandler() 0 3 1
C startElement() 0 18 14
A __construct() 0 10 1
A characterData() 0 4 5
A getCont() 0 16 4
A descriptionEndHandler() 0 3 1
C repeatTagStartHandler() 0 59 14
A brStartHandler() 0 4 3
C factsEndHandler() 0 99 15
C repeatTagEndHandler() 0 90 14
C addAncestors() 0 41 15
A ifEndHandler() 0 4 2
F relativesStartHandler() 0 116 27
B ifStartHandler() 0 61 11
A newPageStartHandler() 0 4 1
F gedcomValueStartHandler() 0 87 21
F addDescendancy() 0 108 30
F setVarStartHandler() 0 131 39
A spStartHandler() 0 4 3
A footnoteTextsStartHandler() 0 4 1
F getGedcomValue() 0 74 26
F listStartHandler() 0 396 81
A substituteVars() 0 18 3
A generationStartHandler() 0 3 1
F varStartHandler() 0 61 19
A footnoteEndHandler() 0 11 3
B highlightedImageStartHandler() 0 42 8
C relativesEndHandler() 0 94 15
A footnoteStartHandler() 0 19 5
C imageStartHandler() 0 59 15
A titleEndHandler() 0 3 1
C listEndHandler() 0 91 14
F factsStartHandler() 0 139 38
F lineStartHandler() 0 49 17
F getPersonNameStartHandler() 0 82 26
A listTotalStartHandler() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like ReportParserGenerate often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ReportParserGenerate, and based on these observations, apply Extract Interface, too.

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

1037
                    $ix1 = strpos(/** @scrutinizer ignore-type */ $name, '</span>', $ix1);
Loading history...
1038
                    if ($ix1 !== false) {   // '«' and '»' mark text for underlining
1039
                        $name = substr_replace($name, '»', $ix1, 7);
1040
                    }
1041
                }
1042
                $addname = strip_tags((string) $tmp[0]['surn']);
1043
                if (!empty($addname) && !($addname === '@N.N.') && !str_contains($name, $addname)) {
1044
                    $name .= " " . $namesep . " " . $addname;
1045
                }
1046
                $this->current_element->addText(trim($name));
1047
            } else {
1048
                $name = $record->fullName();
1049
                $name = strip_tags($name);
1050
                if (!empty($attrs['truncate'])) {
1051
                    if ((int) $attrs['truncate'] > 0) {
1052
                        $name = Str::limit($name, (int) $attrs['truncate'], I18N::translate('…'));
1053
                    }
1054
                } else {
1055
                    $addname = (string) $record->alternateName();
1056
                    $addname = strip_tags($addname);
1057
                    if (!empty($addname)) {
1058
                        $name .= ' ' . $addname;
1059
                    }
1060
                }
1061
                $this->current_element->addText(trim($name));
1062
            }
1063
        }
1064
        if (isset($record) && $famrel && ($this->mfrelation[$record->xref()] != "")) {
1065
            $this->current_element->addText(" (" . (string) $this->mfrelation[$record->xref()] . ")");
1066
        }
1067
    }
1068
1069
    /**
1070
     * Handle <gedcomValue />
1071
     *
1072
     * @param array<string> $attrs
1073
     *
1074
     * @return void
1075
     */
1076
    protected function gedcomValueStartHandler(array $attrs): void
1077
    {
1078
        $id    = '';
1079
        $match = [];
1080
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1081
            $id = $match[1];
1082
        }
1083
1084
        if (isset($attrs['newline']) && $attrs['newline'] === '1') {
1085
            $useBreak = '1';
1086
        } else {
1087
            $useBreak = '0';
1088
        }
1089
1090
        $tag = $attrs['tag'];
1091
        if (!empty($tag)) {
1092
            if ($tag === '@desc') {
1093
                $value = $this->desc;
1094
                $value = trim($value);
1095
                $this->current_element->addText($value);
1096
            }
1097
            if ($tag === '@id') {
1098
                $this->current_element->addText($id);
1099
            } else {
1100
                $tag = str_replace('@fact', $this->fact, $tag);
1101
                if (empty($attrs['level'])) {
1102
                    $level = (int) explode(' ', trim($this->gedrec))[0];
1103
                    if ($level === 0) {
1104
                        $level++;
1105
                    }
1106
                } else {
1107
                    $level = (int) $attrs['level'];
1108
                }
1109
                $tags  = preg_split('/[: ]/', $tag);
1110
                $value = $this->getGedcomValue($tag, $level, $this->gedrec);
1111
                switch (end($tags)) {
1112
                    case 'DATE':
1113
                        $tmp   = new Date($value);
1114
                        $dfmt = "%j %F %Y";
1115
                        if (!empty($attrs['truncate'])) {
1116
                            if ($attrs['truncate'] === "d") {
1117
                                $dfmt = "%j %M %Y";
1118
                            }
1119
                            if ($attrs['truncate'] === "Y") {
1120
                                $dfmt = "%Y";
1121
                            }
1122
                        }
1123
                        $value = strip_tags($tmp->display(null, $dfmt));
1124
                        break;
1125
                    case 'PLAC':
1126
                        $tmp   = new Place($value, $this->tree);
1127
                        $value = $tmp->shortName();
1128
                        break;
1129
                }
1130
                if ($useBreak === '1') {
1131
                    // Insert <br> when multiple dates exist.
1132
                    // This works around a TCPDF bug that incorrectly wraps RTL dates on LTR pages
1133
                    $value = str_replace('(', '<br>(', $value);
1134
                    $value = str_replace('<span dir="ltr"><br>', '<br><span dir="ltr">', $value);
1135
                    $value = str_replace('<span dir="rtl"><br>', '<br><span dir="rtl">', $value);
1136
                    if (substr($value, 0, 4) === '<br>') {
1137
                        $value = substr($value, 4);
1138
                    }
1139
                }
1140
                $tmp = explode(':', $tag);
1141
                if (in_array(end($tmp), ['NOTE', 'TEXT'], true)) {
1142
                    if ($this->tree->getPreference('FORMAT_TEXT') === 'xxmarkdown') {
1143
                        $value = strip_tags(Registry::markdownFactory()->markdown($value, $this->tree), ['br']);
1144
                    } else {
1145
                        $value = str_replace("\n", "<br>", $value);
1146
                        //$value = strip_tags(Registry::markdownFactory()->autolink($value, $this->tree), ['br']);
1147
                    }
1148
                    $value = strtr($value, [MarkdownFactory::BREAK => ' ']);
1149
                }
1150
1151
                if (isset($attrs['lcfirst'])) {
1152
                    $value = lcfirst($value);
1153
                    $value = str_replace(["Å","Ä","Ö"], ["å","ä","ö"], $value);
1154
                }
1155
1156
                if (!empty($attrs['truncate'])) {
1157
                    $value = strip_tags($value);
1158
                    if ((int) $attrs['truncate'] > 0) {
1159
                        $value = Str::limit($value, (int) $attrs['truncate'], I18N::translate('…'));
1160
                    }
1161
                }
1162
                $this->current_element->addText($value);
1163
            }
1164
        }
1165
    }
1166
1167
    /**
1168
     * Handle <repeatTag>
1169
     *
1170
     * @param array<string> $attrs
1171
     *
1172
     * @return void
1173
     */
1174
    protected function repeatTagStartHandler(array $attrs): void
1175
    {
1176
        $this->process_repeats++;
1177
        if ($this->process_repeats > 1) {
1178
            return;
1179
        }
1180
1181
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
1182
        $this->repeats         = [];
1183
        $this->repeat_bytes    = xml_get_current_line_number($this->parser);
1184
1185
        $tag = $attrs['tag'] ?? '';
1186
        if (!empty($tag)) {
1187
            if ($tag === '@desc') {
1188
                $value = $this->desc;
1189
                $value = trim($value);
1190
                $this->current_element->addText($value);
1191
            } else {
1192
                $tag   = str_replace('@fact', $this->fact, $tag);
1193
                $tags  = explode(':', $tag);
1194
                $level = (int) explode(' ', trim($this->gedrec))[0];
1195
                if ($level === 0) {
1196
                    $level++;
1197
                }
1198
                $subrec = $this->gedrec;
1199
                $t      = $tag;
1200
                $count  = count($tags);
1201
                $i      = 0;
1202
                while ($i < $count) {
1203
                    $t = $tags[$i];
1204
                    if (!empty($t)) {
1205
                        if ($i < ($count - 1)) {
1206
                            $subrec = self::getSubRecord($level, "$level $t", $subrec);
1207
                            if (empty($subrec)) {
1208
                                $level--;
1209
                                $subrec = self::getSubRecord($level, "@ $t", $this->gedrec);
1210
                                if (empty($subrec)) {
1211
                                    return;
1212
                                }
1213
                            }
1214
                        }
1215
                        $level++;
1216
                    }
1217
                    $i++;
1218
                }
1219
                $level--;
1220
                $count = preg_match_all("/$level $t(.*)/", $subrec, $match, PREG_SET_ORDER);
1221
                $i     = 0;
1222
                while ($i < $count) {
1223
                    $i++;
1224
                    // Privacy check - is this a link, and are we allowed to view the linked object?
1225
                    $subrecord = self::getSubRecord($level, "$level $t", $subrec, $i);
1226
                    if (preg_match('/^\d ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@/', $subrecord, $xref_match)) {
1227
                        $linked_object = Registry::gedcomRecordFactory()->make($xref_match[1], $this->tree);
1228
                        if ($linked_object && !$linked_object->canShow()) {
1229
                            //continue;
1230
                        }
1231
                    }
1232
                    $this->repeats[] = $subrecord;
1233
                }
1234
            }
1235
        }
1236
    }
1237
1238
    /**
1239
     * Handle </repeatTag>
1240
     *
1241
     * @return void
1242
     */
1243
    protected function repeatTagEndHandler(): void
1244
    {
1245
        $this->process_repeats--;
1246
        if ($this->process_repeats > 0) {
1247
            return;
1248
        }
1249
1250
        $nnnn = count($this->repeats);
0 ignored issues
show
Unused Code introduced by
The assignment to $nnnn is dead and can be removed.
Loading history...
1251
        $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...
1252
        // Check if there is anything to repeat
1253
        if (count($this->repeats) > 0) {
1254
            // No need to load them if not used...
1255
1256
            //-- read the xml from the file
1257
            $lines = file($this->report);
1258
            if (empty($lines)) {
1259
                error_log(__FILE__ . ":" . __LINE__ . " impossible error!? \n");
1260
                // this can not happen! phpstan forces me to add stupid code
1261
                // we come here because the xml file exists! Is it possible to tell phpstan the $lines *is* an array ??
1262
                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...
1263
            }
1264
            $lineoffset = 0;
1265
            foreach ($this->repeats_stack as $rep) {
1266
                if (!empty($rep[1])) {
1267
                    $lineoffset = $lineoffset + (int) ($rep[1]) - 1;
1268
                }
1269
            }
1270
            while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<RepeatTag')) {
1271
                $lineoffset--;
1272
            }
1273
            $lineoffset++;
1274
            $reportxml = "<tempdoc>\n";
1275
            $line_nr   = $lineoffset + $this->repeat_bytes;
1276
            $lnnn = $line_nr;
0 ignored issues
show
Unused Code introduced by
The assignment to $lnnn is dead and can be removed.
Loading history...
1277
            // RepeatTag Level counter
1278
            $count = 1;
1279
            while (0 < $count) {
1280
                if (str_contains($lines[$line_nr], '<RepeatTag')) {
1281
                    $count++;
1282
                } elseif (str_contains($lines[$line_nr], '</RepeatTag')) {
1283
                    $count--;
1284
                }
1285
                if (0 < $count) {
1286
                    $reportxml .= $lines[$line_nr];
1287
                }
1288
                $line_nr++;
1289
            }
1290
            // No need to drag this
1291
            unset($lines);
1292
            $reportxml .= "</tempdoc>\n";
1293
            // Save original values
1294
            $this->parser_stack[] = $this->parser;
1295
            $oldgedrec            = $this->gedrec;
1296
            foreach ($this->repeats as $gedrec) {
1297
                $this->gedrec  = $gedrec;
1298
                $repeat_parser = xml_parser_create();
1299
                $this->parser  = $repeat_parser;
1300
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
1301
1302
                xml_set_element_handler(
1303
                    $repeat_parser,
1304
                    function ($parser, string $name, array $attrs): void {
1305
                        $this->startElement($parser, $name, $attrs);
1306
                    },
1307
                    function ($parser, string $name): void {
1308
                        $this->endElement($parser, $name);
1309
                    }
1310
                );
1311
1312
                xml_set_character_data_handler(
1313
                    $repeat_parser,
1314
                    function ($parser, string $data): void {
1315
                        $this->characterData($parser, $data);
1316
                    }
1317
                );
1318
1319
                if (!xml_parse($repeat_parser, $reportxml, true)) {
1320
                    throw new DomainException(sprintf(
1321
                        'RepeatTagEHandler XML error: %s at line %d',
1322
                        xml_error_string(xml_get_error_code($repeat_parser)),
1323
                        xml_get_current_line_number($repeat_parser)
1324
                    ));
1325
                }
1326
                xml_parser_free($repeat_parser);
1327
            }
1328
            // Restore original values
1329
            $this->gedrec = $oldgedrec;
1330
            $this->parser = array_pop($this->parser_stack);
1331
        }
1332
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
1333
    }
1334
1335
    /**
1336
     * Variable lookup
1337
     * Retrieve predefined variables :
1338
     * @ desc GEDCOM fact description, example:
1339
     *        1 EVEN This is a description
1340
     * @ fact GEDCOM fact tag, such as BIRT, DEAT etc.
1341
     * $ I18N::translate('....')
1342
     * $ language_settings[]
1343
     *
1344
     * @param array<string> $attrs an array of key value pairs for the attributes
1345
     *
1346
     * @return void
1347
     */
1348
    protected function varStartHandler(array $attrs): void
1349
    {
1350
        if (!isset($attrs['var'])) {
1351
            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));
1352
        }
1353
1354
        $var = $attrs['var'];
1355
        // SetVar element preset variables
1356
        if (!empty($this->vars[$var]['id'])) {
1357
            $var = $this->vars[$var]['id'];
1358
        } else {
1359
            $tfact = $this->fact;
1360
            if (($this->fact === 'EVEN' || $this->fact === 'FACT') && $this->type !== '') {
1361
                // Use :
1362
                // n TYPE This text if string
1363
                $tfact = $this->type;
1364
            } else {
1365
                foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) {
1366
                    $element = Registry::elementFactory()->make($record_type . ':' . $this->fact);
1367
1368
                    if (!$element instanceof UnknownElement) {
1369
                        $tfact = $element->label();
1370
                        break;
1371
                    }
1372
                }
1373
            }
1374
1375
            $var = strtr($var, ['@desc' => $this->desc, '@fact' => $tfact]);
1376
1377
            if (preg_match('/^I18N::number\((.+)\)$/', $var, $match)) {
1378
                $var = I18N::number((int) $match[1]);
1379
            } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $var, $match)) {
1380
                $var = I18N::translate($match[1]);
1381
            } elseif (preg_match('/^I18N::translate\(\$(.+)\)$/', $var, $match)) {
1382
                $var = I18N::translate($this->vars[$match[1]]['id']);
1383
            } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $var, $match)) {
1384
                $var = I18N::translateContext($match[1], $match[2]);
1385
            }
1386
        }
1387
        // Check if variable is set as a date and reformat the date
1388
        if (isset($attrs['date'])) {
1389
            if ($attrs['date'] === '1') {
1390
                $g   = new Date($var);
1391
                $var = $g->display();
1392
            }
1393
        }
1394
        if (isset($attrs['amp'])) {
1395
            $var = str_replace("%26", '&', $var);
1396
        }
1397
        if (isset($attrs['cut'])) {
1398
            $cut = (int) $attrs['cut'];
1399
            $var = $cut > 0 ? substr($var, 0, $cut) : substr($var, $cut);
1400
            if ($cut == 0) {
1401
                $var = "";
1402
            }
1403
        }
1404
        if (isset($attrs['lcfirst'])) {
1405
            $var = lcfirst($var);
1406
        }
1407
        $this->current_element->addText($var);
1408
        $this->text = $var; // Used for title/description
1409
    }
1410
1411
    /**
1412
     * Handle <facts>
1413
     *
1414
     * @param array<string> $attrs
1415
     *
1416
     * @return void
1417
     */
1418
    protected function factsStartHandler(array $attrs): void
1419
    {
1420
        $this->process_repeats++;
1421
        if ($this->process_repeats > 1) {
1422
            return;
1423
        }
1424
1425
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
1426
        $this->repeats         = [];
1427
        $this->repeat_bytes    = xml_get_current_line_number($this->parser);
1428
1429
        $id    = '';
1430
        $match = [];
1431
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1432
            $id = $match[1];
1433
        }
1434
        $tag = '';
1435
        if (isset($attrs['ignore'])) {
1436
            $tag .= $attrs['ignore'];
1437
        }
1438
        if (preg_match('/\$(.+)/', $tag, $match)) {
1439
            $tag = $this->vars[$match[1]]['id'];
1440
        }
1441
1442
        $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1443
        if (empty($attrs['diff']) && !empty($id)) {
1444
            $facts = $record->facts([], true);
1445
            $this->repeats = [];
1446
            $nonfacts      = explode(',', $tag);
1447
            foreach ($facts as $fact) {
1448
                $tag = explode(':', $fact->tag())[1];
1449
1450
                if (!in_array($tag, $nonfacts, true)) {
1451
                    $this->repeats[] = $fact->gedcom();
1452
                }
1453
            }
1454
        } else {
1455
            foreach ($record->facts() as $fact) {
1456
                if (($fact->isPendingAddition() || $fact->isPendingDeletion()) && !str_ends_with($fact->tag(), ':CHAN')) {
1457
                    $this->repeats[] = $fact->gedcom();
1458
                }
1459
            }
1460
        }
1461
1462
        $jdarr = [];
1463
        // Add fact/event for FAM:DIV and for death of spouse
1464
        foreach ($this->repeats as $key => $fact) {
1465
            $jdarr[$key] = 0;
1466
            if (preg_match('/1 FAMS @(.+)@/', $fact, $match)) {
1467
                $famid = $match[1];
1468
                $fam = Registry::familyFactory()->make($match[1], $this->tree);
1469
                if ($fam === null) {
1470
                    continue;
1471
                }
1472
                $dt = $this->getGedcomValue("MARR:DATE", 0, $fam->gedcom());
1473
                if ($dt == "") {
1474
                    $dt = $this->getGedcomValue("ENGA:DATE", 0, $fam->gedcom());
1475
                }
1476
                if ($dt == "" && $this->getGedcomValue("EVEN:TYPE", 0, $fam->gedcom()) == "Sambo") {
1477
                    $dt = $this->getGedcomValue("EVEN:DATE", 0, $fam->gedcom());
1478
                }
1479
                $date = new Date($dt);
1480
                $jd = $date->julianDay();
1481
                $jdarr[$key] = $jd;
1482
                // Divorce
1483
                $dt = $this->getGedcomValue("DIV:DATE", 0, $fam->gedcom());
1484
                if ($dt != "") {
1485
                    $this->repeats[] = "1 DIV\n2 DATE " . $dt . "\n";
1486
                }
1487
                // Separation // Doesn't work!! getGedComValue only reports the first event!! I.e. no match here
1488
                if ($this->getGedcomValue("EVEN:TYPE", 0, $fam->gedcom()) == "Separation") {
1489
                    $dt = $this->getGedcomValue("EVEN:DATE", 0, $fam->gedcom());
1490
                    if ($dt != "") {
1491
                        $this->repeats[] = "1 EVEN\n2 TYPE Separation\n2 DATE " . $dt . "\n";
1492
                    }
1493
                }
1494
                // death of husband / wife
1495
                $husb = $fam->husband();
1496
                $wife = $fam->wife();
1497
                if ($this->getGedcomValue("SEX", 0, $this->gedrec) == "M") {
1498
                    $spouse = $wife;
1499
                } else {
1500
                    $spouse = $husb;
1501
                }
1502
                if ($spouse) {
1503
                    $dt = $this->getGedcomValue("DEAT:DATE", 0, $spouse->gedcom());
1504
                } else {
1505
                    $dt = "";
1506
                }
1507
                if ($dt != "") {
1508
                    $this->repeats[] = "1 _SP_DEAT\n2 DATE " . $dt . "\n2 _O_FAM " . $famid . "\n";
1509
                }
1510
            }
1511
        }
1512
        // Find the dates for the facts that are found
1513
        foreach ($this->repeats as $key => $fact) {
1514
            if (preg_match('/[234] DATE ([^\n]+)/', $fact, $match)) {
1515
                $date = new Date($match[1]);
1516
                $jd = $date->julianDay();
1517
                $jdarr[$key] = $jd;
1518
            }
1519
        }
1520
1521
        // Sort facts in chronological order, if possible
1522
        $m = count($this->repeats) - 1;
1523
        $prevd = 0;
1524
        for ($i = 0; $i <= $m; $i++) { // keep undated events after previous dated event
1525
            if ($jdarr[$i] === 0) {
1526
                $jdarr[$i] = $prevd;
1527
            } else {
1528
                $prevd = $jdarr[$i];
1529
            }
1530
        }
1531
1532
        while ($m > 1) {
1533
            $n = count($this->repeats);
1534
            while ($n > 1) {
1535
                if ($jdarr[$n - 2] > $jdarr[$n - 1] && $jdarr[$n - 1] !== 0) {
1536
                    $s = $this->repeats[$n - 1];
1537
                    $this->repeats[$n - 1] = $this->repeats[$n - 2];
1538
                    $this->repeats[$n - 2] = $s;
1539
                    $s = $jdarr[$n - 1];
1540
                    $jdarr[$n - 1] = $jdarr[$n - 2];
1541
                    $jdarr[$n - 2] = $s;
1542
                }
1543
                $n -= 1;
1544
            }
1545
            $m -= 1;
1546
        }
1547
1548
        // Remove spouse deaths that are too late: after new marriage or own death
1549
        $currfam = "";
1550
        for ($i = 0; $i <= count($this->repeats) - 1; $i++) {
1551
            if (preg_match('/[1234] FAMS @(.+)@/', $this->repeats[$i], $match)) {
1552
                $currfam = $match[1];
1553
            }
1554
            if (preg_match('/_SP_DEAT.*\n2 DATE (.*)\n.*_O_FAM (.+)\n/', $this->repeats[$i], $match)) {
1555
                if ($currfam != $match[2] || $i == count($this->repeats) - 1) {
1556
                    $this->repeats[$i] = "1 _XXX\n";
1557
                } // ignore fact
1558
            }
1559
        }
1560
    }
1561
1562
    /**
1563
     * Handle </facts>
1564
     *
1565
     * @return void
1566
     */
1567
    protected function factsEndHandler(): void
1568
    {
1569
        $this->process_repeats--;
1570
        if ($this->process_repeats > 0) {
1571
            return;
1572
        }
1573
1574
        // Check if there is anything to repeat
1575
        if (count($this->repeats) > 0) {
1576
            $line       = xml_get_current_line_number($this->parser) - 1;
1577
            $lineoffset = 0;
1578
            foreach ($this->repeats_stack as $rep) {
1579
                $lineoffset = $lineoffset + (int) ($rep[1]) - 1;
1580
            }
1581
1582
            //-- read the xml from the file
1583
            $lines = file($this->report);
1584
            if (empty($lines)) {
1585
                error_log(__FILE__ . ":" . __LINE__ . " impossible error!? \n");
1586
                // this can not happen! phpstan forces me to add stupid code
1587
                // we come here because the xml file exists! Is it possible to tell phpstan the $lines *is* an array ??
1588
                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...
1589
            }
1590
            while ($lineoffset + $this->repeat_bytes > 0 && !str_contains($lines[$lineoffset + $this->repeat_bytes], '<Facts ')) {
1591
                $lineoffset--;
1592
            }
1593
            $lineoffset++;
1594
            $reportxml = "<tempdoc>\n";
1595
            $i         = $line + $lineoffset;
1596
            $line_nr   = $this->repeat_bytes + $lineoffset;
1597
            while ($line_nr < $i) {
1598
                $reportxml .= $lines[$line_nr];
1599
                $line_nr++;
1600
            }
1601
            // No need to drag this
1602
            unset($lines);
1603
            $reportxml .= "</tempdoc>\n";
1604
            // Save original values
1605
            $this->parser_stack[] = $this->parser;
1606
            $oldgedrec = $this->gedrec;
1607
            $count = count($this->repeats);
1608
            $i = 0;
1609
            while ($i < $count) {
1610
                if (!isset($this->repeats[$i])) {
1611
                    $i++;
1612
                    continue; // this fact has been removed above, occured too late
1613
                }
1614
                $this->gedrec = $this->repeats[$i];
1615
                $this->fact = '';
1616
                $this->desc = '';
1617
                if (preg_match('/1 (\w+)(.*)/', $this->gedrec, $match)) {
1618
                    $this->fact = $match[1];
1619
                    if ($this->fact === 'EVEN' || $this->fact === 'FACT') {
1620
                        $tmatch = [];
1621
                        if (preg_match('/2 TYPE (.+)/', $this->gedrec, $tmatch)) {
1622
                            $this->type = trim($tmatch[1]);
1623
                        } else {
1624
                            $this->type = ' ';
1625
                        }
1626
                    }
1627
                    $this->desc = trim($match[2]);
1628
                    $this->desc .= self::getCont(2, $this->gedrec);
1629
                }
1630
                $repeat_parser = xml_parser_create();
1631
                $this->parser  = $repeat_parser;
1632
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
1633
1634
                xml_set_element_handler(
1635
                    $repeat_parser,
1636
                    function ($parser, string $name, array $attrs): void {
1637
                        $this->startElement($parser, $name, $attrs);
1638
                    },
1639
                    function ($parser, string $name): void {
1640
                        $this->endElement($parser, $name);
1641
                    }
1642
                );
1643
1644
                xml_set_character_data_handler(
1645
                    $repeat_parser,
1646
                    function ($parser, string $data): void {
1647
                        $this->characterData($parser, $data);
1648
                    }
1649
                );
1650
1651
                if (!xml_parse($repeat_parser, $reportxml, true)) {
1652
                    throw new DomainException(sprintf(
1653
                        'FactsEHandler XML error: %s at line %d',
1654
                        xml_error_string(xml_get_error_code($repeat_parser)),
1655
                        xml_get_current_line_number($repeat_parser)
1656
                    ));
1657
                }
1658
                xml_parser_free($repeat_parser);
1659
                $i++;
1660
            }
1661
            // Restore original values
1662
            $this->parser = array_pop($this->parser_stack);
1663
            $this->gedrec = $oldgedrec;
1664
        }
1665
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
1666
    }
1667
1668
    /**
1669
     * Setting upp or changing variables in the XML
1670
     * The XML variable name and value is stored in $this->vars
1671
     *
1672
     * @param array<string> $attrs an array of key value pairs for the attributes
1673
     *
1674
     * @return void
1675
     */
1676
    protected function setVarStartHandler(array $attrs): void
1677
    {
1678
        if (empty($attrs['name'])) {
1679
            throw new DomainException('REPORT ERROR var: The attribute "name" is missing or not set in the XML file');
1680
        }
1681
1682
        $name  = $attrs['name'];
1683
        $value = $attrs['value'];
1684
        if (isset($attrs['dumpvar'])) {
1685
            $dumpvar = $attrs['dumpvar'];
1686
        } else {
1687
            $dumpvar = "";
1688
        }
1689
        $curr_id = "";
1690
        $match = [];
1691
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1692
            $curr_id = $match[1];
1693
        }
1694
        $match = [];
1695
        // Current GEDCOM record strings
1696
        if ($value === '@ID') {
1697
            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1698
                $value = $match[1];
1699
            }
1700
        } elseif ($value === '@fact') {
1701
            $value = $this->fact;
1702
        } elseif ($value === '@desc') {
1703
            $value = $this->desc;
1704
        } elseif ($value === '@format') {
1705
            if (isset($_GET["format"])) {
1706
                $value = $_GET["format"];
1707
            } else {
1708
                $value = "";
1709
            }
1710
        } elseif ($value === '@generation') {
1711
            $value = (string) $this->generation;
1712
        } elseif ($value === '@base_url') {
1713
            $value = "";
1714
            if (array_key_exists("REQUEST_URI", $_SERVER)) {
1715
                $value = urldecode($_SERVER["REQUEST_URI"]);
1716
            }
1717
            $url1 = "";
1718
            $i = strpos($value, "route=");
1719
            if ($i !== false) {
1720
                $url1 = substr($value, 0, $i + 6);
1721
                $value = substr($value, $i + 6);
1722
            }
1723
            $i = strpos($value, "/report");
1724
            if ($i !== false) {
1725
                $value = substr($value, 0, $i);
1726
            }
1727
            $value = $url1 . $value;
1728
        } elseif ($value === '@relation') {
1729
            if (isset($this->mfrelation[$curr_id]) && $curr_id != "") {
1730
                $value = (string) $this->mfrelation[$curr_id];
1731
            } else {
1732
                $value = "";
1733
            }
1734
        } elseif (preg_match("/@(\w+)/", $value, $match)) {
1735
            $gmatch = [];
1736
            if (preg_match("/\d $match[1] (.+)/", $this->gedrec, $gmatch)) {
1737
                $value = str_replace('@', '', trim($gmatch[1]));
1738
            }
1739
        } elseif (preg_match("/@\\$(\w+)/", $value, $match)) {
1740
            if ($match[1] == "dump" && $this->vars['dval']['id'] > 0) {
1741
                // if ($this->vars[ 'dval' ]['id'] == 1001)
1742
                if ($dumpvar == "gedrec") {
1743
                    error_log("\n---- setvar start  " . date("Y-m-d H:i:s") . " RPG " . __LINE__ . "  " . $name . "  gedcom=\n" . $this->gedrec . "\n", 3, "my-errors.log");
1744
                } elseif ($dumpvar != "") {
1745
                    error_log("var: " . $dumpvar . " = " . $this->vars[$dumpvar]['id'] . "\n", 3, "my-errors.log");
1746
                } else {
1747
                    if (array_key_exists('dval', $this->vars)) {
1748
                        $nnn = $this->vars['dval']['id'];
1749
                    } else {
1750
                        $nnn = 0;
1751
                    }
1752
                    error_log("\n---- setvar start  " . date("Y-m-d H:i:s") . " RPG " . __LINE__ . "  " . $name . "  -----\n", 3, "my-errors.log");
1753
                    foreach ($this->vars as $key => $val) {
1754
                        if ($nnn-- < 0) {
1755
                            error_log($key . "='" . $val['id'] . "'\n", 3, "my-errors.log");
1756
                        }
1757
                    }
1758
                }
1759
            }
1760
            $value = $this->vars[$match[1]]['id'];
1761
            if (isset($this->vars[$value]['id'])) {
1762
                $value = '$' . $this->vars[$match[1]]['id'];
1763
            } else {
1764
                $value = "0";
1765
            }
1766
        }
1767
        if (isset($attrs['trim'])) {
1768
            $value = str_replace($attrs['trim'], '', $value);
1769
        }
1770
        if (preg_match("/\\$(\w+)/", $name, $match)) {
1771
            $name = $this->vars["'" . $match[1] . "'"]['id'];
1772
        }
1773
        $count = preg_match_all("/\\$(\w+)/", $value, $match, PREG_SET_ORDER);
1774
        $i     = 0;
1775
        while ($i < $count) {
1776
            $t     = $this->vars[$match[$i][1]]['id'];
1777
            $value = preg_replace('/\$' . $match[$i][1] . '/', $t, $value, 1);
1778
            $i++;
1779
        }
1780
        if (preg_match('/^I18N::number\((.+)\)$/', $value, $match)) {
1781
            $value = I18N::number((int) $match[1]);
1782
        } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $value, $match)) {
1783
            $value = I18N::translate($match[1]);
1784
        } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $value, $match)) {
1785
            $value = I18N::translateContext($match[1], $match[2]);
1786
        }
1787
        if (isset($attrs['lcfirst'])) { // set 1st char to lower case
1788
            $value = lcfirst($value);
1789
        }
1790
1791
        // Arithmetic functions
1792
        if (preg_match("/(\d+)\s*([-+*\/])\s*(\d+)/", $value, $match)) {
1793
            // Create an expression language with the functions used by our reports.
1794
            $expression_provider  = new ReportExpressionLanguageProvider();
1795
            $expression_cache     = new NullAdapter();
1796
            $expression_language  = new ExpressionLanguage($expression_cache, [$expression_provider]);
1797
1798
            $value = (string) $expression_language->evaluate($value);
1799
        }
1800
1801
        if (str_contains($value, '@')) {
1802
            $value = '';
1803
        }
1804
        $this->vars[$name]['id'] = $value;
1805
        if ($name == 'title') {
1806
            $this->wt_report->title = $value;
1807
        }
1808
    }
1809
1810
    /**
1811
     * Handle <if>
1812
     *
1813
     * @param array<string> $attrs
1814
     *
1815
     * @return void
1816
     */
1817
    protected function ifStartHandler(array $attrs): void
1818
    {
1819
        if ($this->process_ifs > 0) {
1820
            $this->process_ifs++;
1821
1822
            return;
1823
        }
1824
1825
        $condition = $attrs['condition'];
1826
        $condition = $this->substituteVars($condition, true);
1827
        $condition = str_replace([
1828
            ' LT ',
1829
            ' GT ',
1830
        ], [
1831
            '<',
1832
            '>',
1833
        ], $condition);
1834
        // Replace the first occurrence only once of @fact:DATE or in any other combinations to the current fact, such as BIRT
1835
        $condition = str_replace('@fact:', $this->fact . ':', $condition);
1836
        $match     = [];
1837
        $count     = preg_match_all("/@([\w:.]+)/", $condition, $match, PREG_SET_ORDER);
1838
        $i         = 0;
1839
        while ($i < $count) {
1840
            $id    = $match[$i][1];
1841
            $value = '""';
1842
            if ($id === 'ID') {
1843
                if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1844
                    $value = "'" . $match[1] . "'";
1845
                }
1846
            } elseif ($id === 'fact') {
1847
                $value = '"' . $this->fact . '"';
1848
            } elseif ($id === 'desc') {
1849
                $value = '"' . addslashes($this->desc) . '"';
1850
            } elseif ($id === 'generation') {
1851
                $value = '"' . $this->generation . '"';
1852
            } else {
1853
                $level = (int) explode(' ', trim($this->gedrec))[0];
1854
                if ($level === 0) {
1855
                    $level++;
1856
                }
1857
                $value = $this->getGedcomValue($id, $level, $this->gedrec);
1858
                if (empty($value)) {
1859
                    $level++;
1860
                    $value = $this->getGedcomValue($id, $level, $this->gedrec);
1861
                }
1862
                $value = preg_replace('/^@(' . Gedcom::REGEX_XREF . ')@$/', '$1', $value);
1863
                $value = '"' . addslashes($value) . '"';
1864
            }
1865
            $condition = str_replace("@$id", $value, $condition);
1866
            $i++;
1867
        }
1868
1869
        // Create an expression language with the functions used by our reports.
1870
        $expression_provider  = new ReportExpressionLanguageProvider();
1871
        $expression_cache     = new NullAdapter();
1872
        $expression_language  = new ExpressionLanguage($expression_cache, [$expression_provider]);
1873
1874
        $ret = $expression_language->evaluate($condition);
1875
1876
        if (!$ret) {
1877
            $this->process_ifs++;
1878
        }
1879
    }
1880
1881
    /**
1882
     * Handle </if>
1883
     *
1884
     * @return void
1885
     */
1886
    protected function ifEndHandler(): void
1887
    {
1888
        if ($this->process_ifs > 0) {
1889
            $this->process_ifs--;
1890
        }
1891
    }
1892
1893
    /**
1894
     * Handle <footnote>
1895
     * Collect the Footnote links
1896
     * GEDCOM Records that are protected by Privacy setting will be ignored
1897
     *
1898
     * @param array<string> $attrs
1899
     *
1900
     * @return void
1901
     */
1902
    protected function footnoteStartHandler(array $attrs): void
1903
    {
1904
        $id = '';
1905
        if (preg_match('/[0-9] (.+) @(.+)@/', $this->gedrec, $match)) {
1906
            $id = $match[2];
1907
        }
1908
        $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1909
        if ($record && $record->canShow()) {
1910
            $this->print_data_stack[] = $this->print_data;
1911
            $this->print_data         = true;
1912
            $style                    = '';
1913
            if (!empty($attrs['style'])) {
1914
                $style = $attrs['style'];
1915
            }
1916
            $this->footnote_element = $this->current_element;
1917
            $this->current_element  = $this->report_root->createFootnote($style);
1918
        } else {
1919
            $this->print_data       = false;
1920
            $this->process_footnote = false;
1921
        }
1922
    }
1923
1924
    /**
1925
     * Handle </footnote>
1926
     * Print the collected Footnote data
1927
     *
1928
     * @return void
1929
     */
1930
    protected function footnoteEndHandler(): void
1931
    {
1932
        if ($this->process_footnote) {
1933
            $this->print_data = array_pop($this->print_data_stack);
1934
            $temp             = trim($this->current_element->getValue());
1935
            if (strlen($temp) > 3) {
1936
                $this->wt_report->addElement($this->current_element);
1937
            }
1938
            $this->current_element = $this->footnote_element;
1939
        } else {
1940
            $this->process_footnote = true;
1941
        }
1942
    }
1943
1944
    /**
1945
     * Handle <footnoteTexts />
1946
     *
1947
     * @return void
1948
     */
1949
    protected function footnoteTextsStartHandler(): void
1950
    {
1951
        $temp = 'footnotetexts';
1952
        $this->wt_report->addElement($temp);
1953
    }
1954
1955
    /**
1956
     * XML element Forced line break handler - HTML code
1957
     *
1958
     * @return void
1959
     */
1960
    protected function brStartHandler(): void
1961
    {
1962
        if ($this->print_data && $this->process_gedcoms === 0) {
1963
            $this->current_element->addText('<br>');
1964
        }
1965
    }
1966
1967
    /**
1968
     * Handle <sp />
1969
     * Forced space
1970
     *
1971
     * @return void
1972
     */
1973
    protected function spStartHandler(): void
1974
    {
1975
        if ($this->print_data && $this->process_gedcoms === 0) {
1976
            $this->current_element->addText(' ');
1977
        }
1978
    }
1979
1980
    /**
1981
     * Handle <highlightedImage />
1982
     *
1983
     * @param array<string> $attrs
1984
     *
1985
     * @return void
1986
     */
1987
    protected function highlightedImageStartHandler(array $attrs): void
1988
    {
1989
        $id = '';
1990
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1991
            $id = $match[1];
1992
        }
1993
1994
        // Position the top corner of this box on the page
1995
        $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION);
1996
1997
        // Position the left corner of this box on the page
1998
        $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION);
1999
2000
        // string Align the image in left, center, right (or empty to use x/y position).
2001
        $align = $attrs['align'] ?? '';
2002
2003
        // string Next Line should be T:next to the image, N:next line
2004
        $ln = $attrs['ln'] ?? 'T';
2005
2006
        // Width, height (or both).
2007
        $width  = (float) ($attrs['width'] ?? 0.0);
2008
        $height = (float) ($attrs['height'] ?? 0.0);
2009
2010
        $person     = Registry::individualFactory()->make($id, $this->tree);
2011
        $media_file = $person->findHighlightedMediaFile();
2012
2013
        if ($media_file instanceof MediaFile && $media_file->fileExists()) {
2014
            $image      = imagecreatefromstring($media_file->fileContents());
2015
            $attributes = [imagesx($image), imagesy($image)];
2016
2017
            if ($width > 0 && $height == 0) {
2018
                $perc   = $width / $attributes[0];
2019
                $height = round($attributes[1] * $perc);
2020
            } elseif ($height > 0 && $width == 0) {
2021
                $perc  = $height / $attributes[1];
2022
                $width = round($attributes[0] * $perc);
2023
            } else {
2024
                $width  = (float) $attributes[0];
2025
                $height = (float) $attributes[1];
2026
            }
2027
            $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln);
2028
            $this->wt_report->addElement($image);
2029
        }
2030
    }
2031
2032
    /**
2033
     * Handle <image/>
2034
     *
2035
     * @param array<string> $attrs
2036
     *
2037
     * @return void
2038
     */
2039
    protected function imageStartHandler(array $attrs): void
2040
    {
2041
        // Position the top corner of this box on the page. the default is the current position
2042
        $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION);
2043
2044
        // mixed Position the left corner of this box on the page. the default is the current position
2045
        $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION);
2046
2047
        // string Align the image in left, center, right (or empty to use x/y position).
2048
        $align = $attrs['align'] ?? '';
2049
2050
        // string Next Line should be T:next to the image, N:next line
2051
        $ln = $attrs['ln'] ?? 'T';
2052
2053
        // Width, height (or both).
2054
        $width  = (float) ($attrs['width'] ?? 0.0);
2055
        $height = (float) ($attrs['height'] ?? 0.0);
2056
2057
        $file = $attrs['file'] ?? '';
2058
2059
        if ($file === '@FILE') {
2060
            $match = [];
2061
            if (preg_match("/\d OBJE @(.+)@/", $this->gedrec, $match)) {
2062
                $mediaobject = Registry::mediaFactory()->make($match[1], $this->tree);
2063
                $media_file  = $mediaobject->firstImageFile();
2064
2065
                if ($media_file instanceof MediaFile && $media_file->fileExists()) {
2066
                    $image      = imagecreatefromstring($media_file->fileContents());
2067
                    $attributes = [imagesx($image), imagesy($image)];
2068
2069
                    if ($width > 0 && $height == 0) {
2070
                        $perc   = $width / $attributes[0];
2071
                        $height = round($attributes[1] * $perc);
2072
                    } elseif ($height > 0 && $width == 0) {
2073
                        $perc  = $height / $attributes[1];
2074
                        $width = round($attributes[0] * $perc);
2075
                    } else {
2076
                        $width  = (float) $attributes[0];
2077
                        $height = (float) $attributes[1];
2078
                    }
2079
                    $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln);
2080
                    $this->wt_report->addElement($image);
2081
                }
2082
            }
2083
        } else {
2084
            if (file_exists($file) && preg_match('/(jpg|jpeg|png|gif)$/i', $file)) {
2085
                $size = getimagesize($file);
2086
                if ($width > 0 && $height == 0) {
2087
                    $perc   = $width / $size[0];
2088
                    $height = round($size[1] * $perc);
2089
                } elseif ($height > 0 && $width == 0) {
2090
                    $perc  = $height / $size[1];
2091
                    $width = round($size[0] * $perc);
2092
                } else {
2093
                    $width  = $size[0];
2094
                    $height = $size[1];
2095
                }
2096
                $image = $this->report_root->createImage($file, $left, $top, $width, $height, $align, $ln);
2097
                $this->wt_report->addElement($image);
2098
            }
2099
        }
2100
    }
2101
2102
    /**
2103
     * Handle <line>
2104
     *
2105
     * @param array<string> $attrs
2106
     *
2107
     * @return void
2108
     */
2109
    protected function lineStartHandler(array $attrs): void
2110
    {
2111
        // Start horizontal position, current position (default)
2112
        $x1 = ReportBaseElement::CURRENT_POSITION;
2113
        if (isset($attrs['x1'])) {
2114
            if ($attrs['x1'] === '0') {
2115
                $x1 = 0;
2116
            } elseif ($attrs['x1'] === '.') {
2117
                $x1 = ReportBaseElement::CURRENT_POSITION;
2118
            } elseif (!empty($attrs['x1'])) {
2119
                $x1 = (float) $attrs['x1'];
2120
            }
2121
        }
2122
        // Start vertical position, current position (default)
2123
        $y1 = ReportBaseElement::CURRENT_POSITION;
2124
        if (isset($attrs['y1'])) {
2125
            if ($attrs['y1'] === '0') {
2126
                $y1 = 0;
2127
            } elseif ($attrs['y1'] === '.') {
2128
                $y1 = ReportBaseElement::CURRENT_POSITION;
2129
            } elseif (!empty($attrs['y1'])) {
2130
                $y1 = (float) $attrs['y1'];
2131
            }
2132
        }
2133
        // End horizontal position, maximum width (default)
2134
        $x2 = ReportBaseElement::CURRENT_POSITION;
2135
        if (isset($attrs['x2'])) {
2136
            if ($attrs['x2'] === '0') {
2137
                $x2 = 0;
2138
            } elseif ($attrs['x2'] === '.') {
2139
                $x2 = ReportBaseElement::CURRENT_POSITION;
2140
            } elseif (!empty($attrs['x2'])) {
2141
                $x2 = (float) $attrs['x2'];
2142
            }
2143
        }
2144
        // End vertical position
2145
        $y2 = ReportBaseElement::CURRENT_POSITION;
2146
        if (isset($attrs['y2'])) {
2147
            if ($attrs['y2'] === '0') {
2148
                $y2 = 0;
2149
            } elseif ($attrs['y2'] === '.') {
2150
                $y2 = ReportBaseElement::CURRENT_POSITION;
2151
            } elseif (!empty($attrs['y2'])) {
2152
                $y2 = (float) $attrs['y2'];
2153
            }
2154
        }
2155
2156
        $line = $this->report_root->createLine($x1, $y1, $x2, $y2);
2157
        $this->wt_report->addElement($line);
2158
    }
2159
2160
    /**
2161
     * Handle <list>
2162
     *
2163
     * @param array<string> $attrs
2164
     *
2165
     * @return void
2166
     */
2167
    protected function listStartHandler(array $attrs): void
2168
    {
2169
        $this->process_repeats++;
2170
        if ($this->process_repeats > 1) {
2171
            return;
2172
        }
2173
2174
        $match = [];
2175
        if (isset($attrs['sortby'])) {
2176
            $sortby = $attrs['sortby'];
2177
            if (preg_match("/\\$(\w+)/", $sortby, $match)) {
2178
                $sortby = $this->vars[$match[1]]['id'];
2179
                $sortby = trim($sortby);
2180
            }
2181
        } else {
2182
            $sortby = 'NAME';
2183
        }
2184
2185
        $listname = $attrs['list'] ?? 'individual';
2186
2187
        // Some filters/sorts can be applied using SQL, while others require PHP
2188
        switch ($listname) {
2189
            case 'pending':
2190
                $this->list = DB::table('change')
2191
                    ->whereIn('change_id', function (Builder $query): void {
2192
                        $query->select([new Expression('MAX(change_id)')])
2193
                            ->from('change')
2194
                            ->where('gedcom_id', '=', $this->tree->id())
2195
                            ->where('status', '=', 'pending')
2196
                            ->groupBy(['xref']);
2197
                    })
2198
                    ->get()
2199
                    ->map(fn (object $row): ?GedcomRecord => Registry::gedcomRecordFactory()->make($row->xref, $this->tree, $row->new_gedcom ?: $row->old_gedcom))
2200
                    ->filter()
2201
                    ->all();
2202
                break;
2203
2204
            case 'individual':
2205
                $query = DB::table('individuals')
2206
                    ->where('i_file', '=', $this->tree->id())
2207
                    ->select(['i_id AS xref', 'i_gedcom AS gedcom'])
2208
                    ->distinct();
2209
2210
                foreach ($attrs as $attr => $value) {
2211
                    if (str_starts_with($attr, 'filter') && $value !== '') {
2212
                        $value = $this->substituteVars($value, false);
2213
                        // Convert the various filters into SQL
2214
                        if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
2215
                            $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2216
                                $join
2217
                                    ->on($attr . '.d_gid', '=', 'i_id')
2218
                                    ->on($attr . '.d_file', '=', 'i_file');
2219
                            });
2220
2221
                            $query->where($attr . '.d_fact', '=', $match[1]);
2222
2223
                            $date = new Date($match[3]);
2224
2225
                            if ($match[2] === 'LTE') {
2226
                                $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay());
2227
                            } else {
2228
                                $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay());
2229
                            }
2230
2231
                            // This filter has been fully processed
2232
                            unset($attrs[$attr]);
2233
                        } elseif (preg_match('/^NAME CONTAINS (.+)$/', $value, $match)) {
2234
                            $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2235
                                $join
2236
                                    ->on($attr . '.n_id', '=', 'i_id')
2237
                                    ->on($attr . '.n_file', '=', 'i_file');
2238
                            });
2239
                            // Search the DB only if there is any name supplied
2240
                            $names = explode(' ', $match[1]);
2241
                            foreach ($names as $name) {
2242
                                $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%');
2243
                            }
2244
2245
                            // This filter has been fully processed
2246
                            unset($attrs[$attr]);
2247
                        } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) {
2248
                            // Convert newline escape sequences to actual new lines
2249
                            $match[1] = str_replace('\n', "\n", $match[1]);
2250
2251
                            $query->where('i_gedcom', 'LIKE', $match[1]);
2252
2253
                            // This filter has been fully processed
2254
                            unset($attrs[$attr]);
2255
                        } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) {
2256
                            // Don't unset this filter. This is just initial filtering for performance
2257
                            $query
2258
                                ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void {
2259
                                    $join
2260
                                        ->on($attr . 'a.pl_file', '=', 'i_file')
2261
                                        ->on($attr . 'a.pl_gid', '=', 'i_id');
2262
                                })
2263
                                ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void {
2264
                                    $join
2265
                                        ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file')
2266
                                        ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id');
2267
                                })
2268
                                ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%');
2269
                        } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) {
2270
                            // Don't unset this filter. This is just initial filtering for performance
2271
                            $match[3] = strtr($match[3], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2272
                            $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%';
2273
                            $query->where('i_gedcom', 'LIKE', $like);
2274
                        } elseif (preg_match('/^(\w+) CONTAINS (.*)$/', $value, $match)) {
2275
                            // Don't unset this filter. This is just initial filtering for performance
2276
                            $match[2] = strtr($match[2], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2277
                            $like = "%\n1 " . $match[1] . '%' . $match[2] . '%';
2278
                            $query->where('i_gedcom', 'LIKE', $like);
2279
                        }
2280
                    }
2281
                }
2282
2283
                $this->list = [];
2284
2285
                foreach ($query->get() as $row) {
2286
                    $this->list[$row->xref] = Registry::individualFactory()->make($row->xref, $this->tree, $row->gedcom);
2287
                }
2288
                break;
2289
2290
            case 'family':
2291
                $query = DB::table('families')
2292
                    ->where('f_file', '=', $this->tree->id())
2293
                    ->select(['f_id AS xref', 'f_gedcom AS gedcom'])
2294
                    ->distinct();
2295
2296
                foreach ($attrs as $attr => $value) {
2297
                    if (str_starts_with($attr, 'filter') && $value !== '') {
2298
                        $value = $this->substituteVars($value, false);
2299
                        // Convert the various filters into SQL
2300
                        if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
2301
                            $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2302
                                $join
2303
                                    ->on($attr . '.d_gid', '=', 'f_id')
2304
                                    ->on($attr . '.d_file', '=', 'f_file');
2305
                            });
2306
2307
                            $query->where($attr . '.d_fact', '=', $match[1]);
2308
2309
                            $date = new Date($match[3]);
2310
2311
                            if ($match[2] === 'LTE') {
2312
                                $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay());
2313
                            } else {
2314
                                $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay());
2315
                            }
2316
2317
                            // This filter has been fully processed
2318
                            unset($attrs[$attr]);
2319
                        } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) {
2320
                            // Convert newline escape sequences to actual new lines
2321
                            $match[1] = str_replace('\n', "\n", $match[1]);
2322
2323
                            $query->where('f_gedcom', 'LIKE', $match[1]);
2324
2325
                            // This filter has been fully processed
2326
                            unset($attrs[$attr]);
2327
                        } elseif (preg_match('/^NAME CONTAINS (.*)$/', $value, $match)) {
2328
                            if ($sortby === 'NAME' || $match[1] !== '') {
2329
                                $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2330
                                    $join
2331
                                        ->on($attr . '.n_file', '=', 'f_file')
2332
                                        ->where(static function (Builder $query): void {
2333
                                            $query
2334
                                                ->whereColumn('n_id', '=', 'f_husb')
2335
                                                ->orWhereColumn('n_id', '=', 'f_wife');
2336
                                        });
2337
                                });
2338
                                // Search the DB only if there is any name supplied
2339
                                if ($match[1] != '') {
2340
                                    $names = explode(' ', $match[1]);
2341
                                    foreach ($names as $name) {
2342
                                        $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%');
2343
                                    }
2344
                                }
2345
                            }
2346
2347
                            // This filter has been fully processed
2348
                            unset($attrs[$attr]);
2349
                        } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) {
2350
                            // Don't unset this filter. This is just initial filtering for performance
2351
                            $query
2352
                                ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void {
2353
                                    $join
2354
                                        ->on($attr . 'a.pl_file', '=', 'f_file')
2355
                                        ->on($attr . 'a.pl_gid', '=', 'f_id');
2356
                                })
2357
                                ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void {
2358
                                    $join
2359
                                        ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file')
2360
                                        ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id');
2361
                                })
2362
                                ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%');
2363
                        } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) {
2364
                            // Don't unset this filter. This is just initial filtering for performance
2365
                            $match[3] = strtr($match[3], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2366
                            $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%';
2367
                            $query->where('f_gedcom', 'LIKE', $like);
2368
                        } elseif (preg_match('/^(\w+) CONTAINS (.+)$/', $value, $match)) {
2369
                            // Don't unset this filter. This is just initial filtering for performance
2370
                            $match[2] = strtr($match[2], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2371
                            $like = "%\n1 " . $match[1] . '%' . $match[2] . '%';
2372
                            $query->where('f_gedcom', 'LIKE', $like);
2373
                        }
2374
                    }
2375
                }
2376
2377
                $this->list = [];
2378
2379
                foreach ($query->get() as $row) {
2380
                    $this->list[$row->xref] = Registry::familyFactory()->make($row->xref, $this->tree, $row->gedcom);
2381
                }
2382
                break;
2383
2384
            default:
2385
                throw new DomainException('Invalid list name: ' . $listname);
2386
        }
2387
2388
        $filters  = [];
2389
        $filters2 = [];
2390
        if (isset($attrs['filter1']) && count($this->list) > 0) {
2391
            foreach ($attrs as $key => $value) {
2392
                if (preg_match("/filter(\d)/", $key)) {
2393
                    $condition = $value;
2394
                    if (preg_match("/@(\w+)/", $condition, $match)) {
2395
                        $id    = $match[1];
2396
                        $value = "''";
2397
                        if ($id === 'ID') {
2398
                            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
2399
                                $value = "'" . $match[1] . "'";
2400
                            }
2401
                        } elseif ($id === 'fact') {
2402
                            $value = "'" . $this->fact . "'";
2403
                        } elseif ($id === 'desc') {
2404
                            $value = "'" . $this->desc . "'";
2405
                        } else {
2406
                            if (preg_match("/\d $id (.+)/", $this->gedrec, $match)) {
2407
                                $value = "'" . str_replace('@', '', trim($match[1])) . "'";
2408
                            }
2409
                        }
2410
                        $condition = preg_replace("/@$id/", $value, $condition);
2411
                    }
2412
                    //-- handle regular expressions
2413
                    if (preg_match("/([A-Z:]+)\s*([^\s]+)\s*(.+)/", $condition, $match)) {
2414
                        $tag  = trim($match[1]);
2415
                        $expr = trim($match[2]);
2416
                        $val  = trim($match[3]);
2417
                        if (preg_match("/\\$(\w+)/", $val, $match)) {
2418
                            $val = $this->vars[$match[1]]['id'];
2419
                            $val = trim($val);
2420
                        }
2421
                        if ($val !== '') {
2422
                            $searchstr = '';
2423
                            $tags      = explode(':', $tag);
2424
                            //-- only limit to a level number if we are specifically looking at a level
2425
                            if (count($tags) > 1) {
2426
                                $level = 1;
2427
                                $t = 'XXXX';
2428
                                foreach ($tags as $t) {
2429
                                    if (!empty($searchstr)) {
2430
                                        $searchstr .= "[^\n]*(\n[2-9][^\n]*)*\n";
2431
                                    }
2432
                                    //-- search for both EMAIL and _EMAIL... silly double gedcom standard
2433
                                    if ($t === 'EMAIL' || $t === '_EMAIL') {
2434
                                        $t = '_?EMAIL';
2435
                                    }
2436
                                    $searchstr .= $level . ' ' . $t;
2437
                                    $level++;
2438
                                }
2439
                            } else {
2440
                                if ($tag === 'EMAIL' || $tag === '_EMAIL') {
2441
                                    $tag = '_?EMAIL';
2442
                                }
2443
                                $t         = $tag;
2444
                                $searchstr = '1 ' . $tag;
2445
                            }
2446
                            switch ($expr) {
2447
                                case 'CONTAINS':
2448
                                    if ($t === 'PLAC') {
2449
                                        $searchstr .= "[^\n]*[, ]*" . $val;
2450
                                    } else {
2451
                                        $searchstr .= "[^\n]*" . $val;
2452
                                    }
2453
                                    $filters[] = $searchstr;
2454
                                    break;
2455
                                default:
2456
                                    $filters2[] = [
2457
                                        'tag'  => $tag,
2458
                                        'expr' => $expr,
2459
                                        'val'  => $val,
2460
                                    ];
2461
                                    break;
2462
                            }
2463
                        }
2464
                    }
2465
                }
2466
            }
2467
        }
2468
        //-- apply other filters to the list that could not be added to the search string
2469
        if ($filters !== []) {
2470
            foreach ($this->list as $key => $record) {
2471
                foreach ($filters as $filter) {
2472
                    if (!preg_match('/' . $filter . '/i', $record->privatizeGedcom(Auth::accessLevel($this->tree)))) {
2473
                        unset($this->list[$key]);
2474
                        break;
2475
                    }
2476
                }
2477
            }
2478
        }
2479
        if ($filters2 !== []) {
2480
            $mylist = [];
2481
            foreach ($this->list as $indi) {
2482
                $key  = $indi->xref();
2483
                $grec = $indi->privatizeGedcom(Auth::accessLevel($this->tree));
2484
                $keep = true;
2485
                foreach ($filters2 as $filter) {
2486
                    if ($keep) {
2487
                        $tag  = $filter['tag'];
2488
                        $expr = $filter['expr'];
2489
                        $val  = $filter['val'];
2490
                        if ($val === "''") {
2491
                            $val = '';
2492
                        }
2493
                        $tags = explode(':', $tag);
2494
                        $t    = end($tags);
2495
                        $v    = $this->getGedcomValue($tag, 1, $grec);
2496
                        //-- check for EMAIL and _EMAIL (silly double gedcom standard :P)
2497
                        if ($t === 'EMAIL' && empty($v)) {
2498
                            $tag  = str_replace('EMAIL', '_EMAIL', $tag);
2499
                            $tags = explode(':', $tag);
2500
                            $t    = end($tags);
2501
                            $v    = self::getSubRecord(1, $tag, $grec);
2502
                        }
2503
2504
                        switch ($expr) {
2505
                            case 'GTE':
2506
                                if ($t === 'DATE') {
2507
                                    $date1 = new Date($v);
2508
                                    $date2 = new Date($val);
2509
                                    $keep  = (Date::compare($date1, $date2) >= 0);
2510
                                } elseif ($val >= $v) {
2511
                                    $keep = true;
2512
                                }
2513
                                break;
2514
                            case 'LTE':
2515
                                if ($t === 'DATE') {
2516
                                    $date1 = new Date($v);
2517
                                    $date2 = new Date($val);
2518
                                    $keep  = (Date::compare($date1, $date2) <= 0);
2519
                                } elseif ($val >= $v) {
2520
                                    $keep = true;
2521
                                }
2522
                                break;
2523
                            default:
2524
                                if ($v == $val) {
2525
                                    $keep = true;
2526
                                } else {
2527
                                    $keep = false;
2528
                                }
2529
                                break;
2530
                        }
2531
                    }
2532
                }
2533
                if ($keep) {
2534
                    $mylist[$key] = $indi;
2535
                }
2536
            }
2537
            $this->list = $mylist;
2538
        }
2539
2540
        switch ($sortby) {
2541
            case 'NAME':
2542
                uasort($this->list, GedcomRecord::nameComparator());
2543
                break;
2544
            case 'CHAN':
2545
                uasort($this->list, GedcomRecord::lastChangeComparator());
2546
                break;
2547
            case 'BIRT:DATE':
2548
                uasort($this->list, Individual::birthDateComparator());
2549
                break;
2550
            case 'DEAT:DATE':
2551
                uasort($this->list, Individual::deathDateComparator());
2552
                break;
2553
            case 'MARR:DATE':
2554
                uasort($this->list, Family::marriageDateComparator());
2555
                break;
2556
            default:
2557
                // unsorted or already sorted by SQL
2558
                break;
2559
        }
2560
2561
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
2562
        $this->repeat_bytes    = xml_get_current_line_number($this->parser) + 1;
2563
    }
2564
2565
    /**
2566
     * Handle </list>
2567
     *
2568
     * @return void
2569
     */
2570
    protected function listEndHandler(): void
2571
    {
2572
        $this->process_repeats--;
2573
        if ($this->process_repeats > 0) {
2574
            return;
2575
        }
2576
2577
        // Check if there is any list
2578
        if (count($this->list) > 0) {
2579
            $lineoffset = 0;
2580
            foreach ($this->repeats_stack as $rep) {
2581
                $lineoffset = $lineoffset + (int) ($rep[1]) - 1;
2582
            }
2583
            //-- read the xml from the file
2584
            $lines = file($this->report);
2585
            if (empty($lines)) {
2586
                error_log(__FILE__ . ":" . __LINE__ . " impossible error!? \n");
2587
                // this can not happen! phpstan forces me to add stupid code
2588
                // we come here because the xml file exists! Is it possible to tell phpstan the $lines *is* an array ??
2589
                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...
2590
            }
2591
            while ((!str_contains($lines[$lineoffset + $this->repeat_bytes], '<List')) && (($lineoffset + $this->repeat_bytes) > 0)) {
2592
                $lineoffset--;
2593
            }
2594
            $lineoffset++;
2595
            $reportxml = "<tempdoc>\n";
2596
            $line_nr   = $lineoffset + $this->repeat_bytes;
2597
            // List Level counter
2598
            $count = 1;
2599
            while (0 < $count) {
2600
                if (str_contains($lines[$line_nr], '<List')) {
2601
                    $count++;
2602
                } elseif (str_contains($lines[$line_nr], '</List')) {
2603
                    $count--;
2604
                }
2605
                if (0 < $count) {
2606
                    $reportxml .= $lines[$line_nr];
2607
                }
2608
                $line_nr++;
2609
            }
2610
            // No need to drag this
2611
            unset($lines);
2612
            $reportxml .= '</tempdoc>';
2613
            // Save original values
2614
            $this->parser_stack[] = $this->parser;
2615
            $oldgedrec            = $this->gedrec;
2616
2617
            $this->list_total   = count($this->list);
2618
            $this->list_private = 0;
2619
            foreach ($this->list as $record) {
2620
                if ($record->canShow()) {
2621
                    $this->gedrec = $record->privatizeGedcom(Auth::accessLevel($record->tree()));
2622
                    //-- start the sax parser
2623
                    $repeat_parser = xml_parser_create();
2624
                    $this->parser  = $repeat_parser;
2625
                    xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
2626
2627
                    xml_set_element_handler(
2628
                        $repeat_parser,
2629
                        function ($parser, string $name, array $attrs): void {
2630
                            $this->startElement($parser, $name, $attrs);
2631
                        },
2632
                        function ($parser, string $name): void {
2633
                            $this->endElement($parser, $name);
2634
                        }
2635
                    );
2636
2637
                    xml_set_character_data_handler(
2638
                        $repeat_parser,
2639
                        function ($parser, string $data): void {
2640
                            $this->characterData($parser, $data);
2641
                        }
2642
                    );
2643
2644
                    if (!xml_parse($repeat_parser, $reportxml, true)) {
2645
                        throw new DomainException(sprintf(
2646
                            'ListEHandler XML error: %s at line %d',
2647
                            xml_error_string(xml_get_error_code($repeat_parser)),
2648
                            xml_get_current_line_number($repeat_parser)
2649
                        ));
2650
                    }
2651
                    xml_parser_free($repeat_parser);
2652
                } else {
2653
                    $this->list_private++;
2654
                }
2655
            }
2656
            $this->list   = [];
2657
            $this->parser = array_pop($this->parser_stack);
2658
            $this->gedrec = $oldgedrec;
2659
        }
2660
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
2661
    }
2662
2663
    /**
2664
     * Handle <listTotal>
2665
     * Prints the total number of records in a list
2666
     * The total number is collected from <list> and <relatives>
2667
     *
2668
     * @return void
2669
     */
2670
    protected function listTotalStartHandler(): void
2671
    {
2672
        if ($this->list_private == 0) {
2673
            $this->current_element->addText((string) $this->list_total);
2674
        } else {
2675
            $this->current_element->addText(($this->list_total - $this->list_private) . ' / ' . $this->list_total);
2676
        }
2677
    }
2678
2679
    /**
2680
     * Handle <relatives>
2681
     *
2682
     * @param array<string> $attrs
2683
     *
2684
     * @return void
2685
     */
2686
    protected function relativesStartHandler(array $attrs): void
2687
    {
2688
        $this->process_repeats++;
2689
        if ($this->process_repeats > 1) {
2690
            return;
2691
        }
2692
2693
        $sortby = $attrs['sortby'] ?? 'NAME';
2694
2695
        $match = [];
2696
        if (preg_match("/\\$(\w+)/", $sortby, $match)) {
2697
            $sortby = $this->vars[$match[1]]['id'];
2698
            $sortby = trim($sortby);
2699
        }
2700
2701
        $maxgen = -1;
2702
        if (isset($attrs['maxgen'])) {
2703
            $maxgen = (int) $attrs['maxgen'];
2704
        }
2705
2706
        $group = $attrs['group'] ?? 'child-family';
2707
2708
        if (preg_match("/\\$(\w+)/", $group, $match)) {
2709
            $group = $this->vars[$match[1]]['id'];
2710
            $group = trim($group);
2711
        }
2712
2713
        $id = $attrs['id'] ?? '';
2714
2715
        if (preg_match("/\\$(\w+)/", $id, $match)) {
2716
            $id = $this->vars[$match[1]]['id'];
2717
            $id = trim($id);
2718
        }
2719
2720
        $this->list = [];
2721
        $person     = Registry::individualFactory()->make($id, $this->tree);
2722
        if ($person instanceof Individual) {
2723
            $this->list[$id] = $person;
2724
            $this->mfrelation[$id] = "";
2725
            $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...
2726
            switch ($group) {
2727
                case 'child-family':
2728
                    foreach ($person->childFamilies() as $family) {
2729
                        foreach ($family->spouses() as $spouse) {
2730
                            $this->list[$spouse->xref()] = $spouse;
2731
                        }
2732
2733
                        foreach ($family->children() as $child) {
2734
                            $this->list[$child->xref()] = $child;
2735
                        }
2736
                    }
2737
                    break;
2738
                case 'spouse-family':
2739
                    foreach ($person->spouseFamilies() as $family) {
2740
                        foreach ($family->spouses() as $spouse) {
2741
                            $this->list[$spouse->xref()] = $spouse;
2742
                        }
2743
2744
                        foreach ($family->children() as $child) {
2745
                            $this->list[$child->xref()] = $child;
2746
                        }
2747
                    }
2748
                    break;
2749
                case 'direct-ancestors':
2750
                    $this->addAncestors($this->list, $id, false, $maxgen);
2751
                    break;
2752
                case 'ancestors':
2753
                    $this->addAncestors($this->list, $id, true, $maxgen);
2754
                    break;
2755
                case 'descendants':
2756
                    $this->list[$id]->generation = 1;
2757
                    $this->addDescendancy($this->list, $id, false, $maxgen);
2758
                    break;
2759
                case 'all':
2760
                    $this->addAncestors($this->list, $id, true, $maxgen);
2761
                    $this->addDescendancy($this->list, $id, true, $maxgen);
2762
                    break;
2763
            }
2764
        }
2765
2766
        switch ($sortby) {
2767
            case 'NAME':
2768
                uasort($this->list, GedcomRecord::nameComparator());
2769
                break;
2770
            case 'BIRT:DATE':
2771
                uasort($this->list, Individual::birthDateComparator());
2772
                break;
2773
            case 'DEAT:DATE':
2774
                uasort($this->list, Individual::deathDateComparator());
2775
                break;
2776
            case 'generation':
2777
                $newarray = [];
2778
                reset($this->list);
2779
                $genCounter = 1;
2780
                while (count($newarray) < count($this->list)) {
2781
                    foreach ($this->list as $key => $value) {
2782
                        if ($value->generation < 0) {
2783
                            // indication of husband or wife
2784
                            $this->generation = -$value->generation;
2785
                        } else {
2786
                            $this->generation = $value->generation;
2787
                        }
2788
                        if ($this->generation == $genCounter) {
2789
                            $newarray[$key] = (object) ['generation' => $this->generation];
2790
                        }
2791
                    }
2792
                    $genCounter++;
2793
                }
2794
                $this->list = $newarray;
2795
                break;
2796
            default:
2797
                // unsorted
2798
                break;
2799
        }
2800
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
2801
        $this->repeat_bytes    = xml_get_current_line_number($this->parser) + 1;
2802
    }
2803
2804
    /**
2805
     * Handle </relatives>
2806
     *
2807
     * @return void
2808
     */
2809
    protected function relativesEndHandler(): void
2810
    {
2811
        $this->process_repeats--;
2812
        if ($this->process_repeats > 0) {
2813
            return;
2814
        }
2815
2816
        // Check if there is any relatives
2817
        if (count($this->list) > 0) {
2818
            $lineoffset = 0;
2819
            foreach ($this->repeats_stack as $rep) {
2820
                $lineoffset = $lineoffset + (int) ($rep[1]) - 1;
2821
            }
2822
            //-- read the xml from the file
2823
            $lines = file($this->report);
2824
            if (empty($lines)) {
2825
                error_log(__FILE__ . ":" . __LINE__ . " impossible error!? \n");
2826
                // this can not happen! phpstan forces me to add stupid code
2827
                // we come here because the xml file exists! Is it possible to tell phpstan the $lines *is* an array ??
2828
                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...
2829
            }
2830
            while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<Relatives') && $lineoffset + $this->repeat_bytes > 0) {
2831
                $lineoffset--;
2832
            }
2833
            $lineoffset++;
2834
            $reportxml = "<tempdoc>\n";
2835
            $line_nr   = $lineoffset + $this->repeat_bytes;
2836
            // Relatives Level counter
2837
            $count = 1;
2838
            while (0 < $count) {
2839
                if (str_contains($lines[$line_nr], '<Relatives')) {
2840
                    $count++;
2841
                } elseif (str_contains($lines[$line_nr], '</Relatives')) {
2842
                    $count--;
2843
                }
2844
                if (0 < $count) {
2845
                    $reportxml .= $lines[$line_nr];
2846
                }
2847
                $line_nr++;
2848
            }
2849
            // No need to drag this
2850
            unset($lines);
2851
            $reportxml .= "</tempdoc>\n";
2852
            // Save original values
2853
            $this->parser_stack[] = $this->parser;
2854
            $oldgedrec            = $this->gedrec;
2855
2856
            $this->list_total   = count($this->list);
2857
            $this->list_private = 0;
2858
            foreach ($this->list as $key => $value) {
2859
                if (isset($value->generation)) {
2860
                    $this->generation = $value->generation;
2861
                }
2862
                $xref = $key;
2863
                $this->vars["dupl"]["id"] = "no";
2864
                if (substr($key, 0, 2) == "D_") {
2865
                    $xref = substr($key, strrpos($key, "_") + 1);
2866
                    $this->vars["dupl"]["id"] = "yes";
2867
                }
2868
                $tmp          = Registry::gedcomRecordFactory()->make((string) $xref, $this->tree);
2869
                $this->gedrec = $tmp->privatizeGedcom(Auth::accessLevel($this->tree));
2870
2871
                $repeat_parser = xml_parser_create();
2872
                $this->parser  = $repeat_parser;
2873
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
2874
2875
                xml_set_element_handler(
2876
                    $repeat_parser,
2877
                    function ($parser, string $name, array $attrs): void {
2878
                        $this->startElement($parser, $name, $attrs);
2879
                    },
2880
                    function ($parser, string $name): void {
2881
                        $this->endElement($parser, $name);
2882
                    }
2883
                );
2884
2885
                xml_set_character_data_handler(
2886
                    $repeat_parser,
2887
                    function ($parser, string $data): void {
2888
                        $this->characterData($parser, $data);
2889
                    }
2890
                );
2891
2892
                if (!xml_parse($repeat_parser, $reportxml, true)) {
2893
                    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)));
2894
                }
2895
                xml_parser_free($repeat_parser);
2896
            }
2897
            // Clean up the list array
2898
            $this->list   = [];
2899
            $this->parser = array_pop($this->parser_stack);
2900
            $this->gedrec = $oldgedrec;
2901
        }
2902
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
2903
    }
2904
2905
    /**
2906
     * Handle <generation />
2907
     * Prints the number of generations
2908
     *
2909
     * @return void
2910
     */
2911
    protected function generationStartHandler(): void
2912
    {
2913
        $this->current_element->addText((string) $this->generation);
2914
    }
2915
2916
    /**
2917
     * Handle <newPage />
2918
     * Has to be placed in an element (header, body or footer)
2919
     *
2920
     * @return void
2921
     */
2922
    protected function newPageStartHandler(): void
2923
    {
2924
        $temp = 'addpage';
2925
        $this->wt_report->addElement($temp);
2926
    }
2927
2928
    /**
2929
     * Handle </title>
2930
     *
2931
     * @return void
2932
     */
2933
    protected function titleEndHandler(): void
2934
    {
2935
        $this->report_root->addTitle($this->text);
2936
    }
2937
2938
    /**
2939
     * Handle </description>
2940
     *
2941
     * @return void
2942
     */
2943
    protected function descriptionEndHandler(): void
2944
    {
2945
        $this->report_root->addDescription($this->text);
2946
    }
2947
2948
    /**
2949
     * Create a list of all descendants.
2950
     *
2951
     * @param array<Individual> $list
2952
     * @param string            $pid
2953
     * @param bool              $parents
2954
     * @param int               $generations
2955
     *
2956
     * @return void
2957
     */
2958
    private function addDescendancy(&$list, $pid, $parents = false, $generations = -1): void
2959
    {
2960
        $person = Registry::individualFactory()->make($pid, $this->tree);
2961
        if ($person === null) {
2962
            return;
2963
        }
2964
2965
        static $focusperson = true;
2966
        static $dupl = 1;
2967
        $sx = $person->sex();
2968
        $rl = "x"; // unknown
2969
        if ($sx == "M") {
2970
            $rl = "s";
2971
        } // son
2972
        if ($sx == "F") {
2973
            $rl = "d";
0 ignored issues
show
Unused Code introduced by
The assignment to $rl is dead and can be removed.
Loading history...
2974
        } // daughter
2975
        if ($focusperson) {
2976
            $this->mfrelation[$pid] = "";
2977
        }
2978
        $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...
2979
2980
        $newpid = $pid;
2981
        if (!isset($list[$pid])) {
2982
            $list[$pid] = $person;
2983
        } elseif (!$focusperson) {
2984
            $newpid = "D_" . $dupl . "_" . $pid;
2985
            $list[$newpid] = $person;
2986
        }
2987
        if (!isset($list[$newpid]->generation)) {
2988
            $list[$newpid]->generation = 0;
2989
        }
2990
        $focusperson = false;
2991
        foreach ($person->spouseFamilies() as $family) {
2992
            if ($parents) {
2993
                $husband = $family->husband();
2994
                $wife    = $family->wife();
2995
                if ($husband) {
2996
                    $list[$husband->xref()] = $husband;
2997
                    if (isset($list[$pid]->generation)) {
2998
                        $list[$husband->xref()]->generation = $list[$pid]->generation - 1;
2999
                    } else {
3000
                        $list[$husband->xref()]->generation = 1;
3001
                    }
3002
                }
3003
                if ($wife) {
3004
                    $list[$wife->xref()] = $wife;
3005
                    if (isset($list[$pid]->generation)) {
3006
                        $list[$wife->xref()]->generation = $list[$pid]->generation - 1;
3007
                    } else {
3008
                        $list[$wife->xref()]->generation = 1;
3009
                    }
3010
                }
3011
            }
3012
            $husband = $family->husband();
3013
            $wife = $family->wife();
3014
3015
            if ($husband && $wife) {
3016
                if ($husband->xref() == $person->xref()) {
3017
                    $this->mfrelation[$wife->xref()] = $this->mfrelation[$person->xref()] . "x";
3018
                    if ($wife->canShow()) {
3019
                        $list[$wife->xref()] = $wife;
3020
                    }
3021
                    if (!isset($wife->generation)) {
3022
                        $wife->generation = $person->generation;
3023
                    }
3024
                    $nam = $wife->getAllNames()[0]['fullNN'];
3025
                } else {
3026
                    $this->mfrelation[$husband->xref()] = $this->mfrelation[$person->xref()] . "x";
3027
                    if ($husband->canShow()) {
3028
                        $list[$husband->xref()] = $husband;
3029
                    }
3030
                    if (!isset($husband->generation)) {
3031
                        $husband->generation = $person->generation;
3032
                    }
3033
                    $nam = $husband->getAllNames()[0]['fullNN'];
3034
                }
3035
            }
3036
3037
            $children = $family->children();
3038
            foreach ($children as $child) {
3039
                if ($child) {
3040
                    $sx = $child->sex();
3041
                    $rl = "x"; // unknown
3042
                    if ($sx == "M") {
3043
                        $rl = "s";
3044
                    } // son
3045
                    if ($sx == "F") {
3046
                        $rl = "d";
3047
                    } // daughter
3048
                    $rl = $this->mfrelation[$person->xref()] . $rl;
3049
                    $this->mfrelation[$child->xref()] = $rl;
3050
                    if (isset($list[$pid]->generation)) {
3051
                        $child->generation = $list[$pid]->generation + 1;
3052
                    } else {
3053
                        $child->generation = 2;
3054
                    }
3055
                }
3056
            }
3057
            if ($generations == -1 || $list[$pid]->generation < $generations) {
3058
                foreach ($children as $child) {
3059
                    if ($child->canShow()) {
3060
                        $this->addDescendancy($list, $child->xref(), $parents, $generations);
3061
                    } // recurse on the childs family
3062
                }
3063
            }
3064
        }
3065
        $focusperson = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $focusperson is dead and can be removed.
Loading history...
3066
    }
3067
3068
    /**
3069
     * Create a list of all ancestors.
3070
     *
3071
     * @param array<Individual> $list
3072
     * @param string            $pid
3073
     * @param bool              $children
3074
     * @param int               $generations
3075
     *
3076
     * @return void
3077
     */
3078
    private function addAncestors(array &$list, string $pid, bool $children = false, int $generations = -1): void
3079
    {
3080
        $genlist                = [$pid];
3081
        $list[$pid]->generation = 1;
3082
        while (count($genlist) > 0) {
3083
            $id = array_shift($genlist);
3084
            if (str_starts_with($id, 'empty')) {
3085
                continue; // id can be something like “empty7”
3086
            }
3087
            if (!isset($this->mfrelation[$id])) {
3088
                $this->mfrelation[$id] = "";
3089
            }
3090
            $person = Registry::individualFactory()->make($id, $this->tree);
3091
            foreach ($person->childFamilies() as $family) {
3092
                $husband = $family->husband();
3093
                $wife    = $family->wife();
3094
                if ($husband) {
3095
                    $list[$husband->xref()]             = $husband;
3096
                    $list[$husband->xref()]->generation = $list[$id]->generation + 1;
3097
                    $this->mfrelation[$husband->xref()] = $this->mfrelation[$id] . "f";
3098
                }
3099
                if ($wife) {
3100
                    $list[$wife->xref()]             = $wife;
3101
                    $list[$wife->xref()]->generation = $list[$id]->generation + 1;
3102
                    $this->mfrelation[$wife->xref()] = $this->mfrelation[$id] . "m";
3103
                }
3104
                if ($generations == -1 || $list[$id]->generation + 1 < $generations) {
3105
                    if ($husband) {
3106
                        $genlist[] = $husband->xref();
3107
                    }
3108
                    if ($wife) {
3109
                        $genlist[] = $wife->xref();
3110
                    }
3111
                }
3112
                if ($children && isset($person)) {
3113
                    // unnecessary test of $person to satisfy phpstan!
3114
                    foreach ($family->children() as $child) {
3115
                        $list[$child->xref()] = $child;
3116
                        $child->generation = $list[$id]->generation ?? 1;
3117
                        if ($child->xref() != $person->xref()) {
3118
                            $this->mfrelation[$child->xref()] = $this->mfrelation[$id] . "x";
3119
                        }
3120
                    }
3121
                }
3122
            }
3123
        }
3124
    }
3125
3126
    /**
3127
     * get gedcom tag value
3128
     *
3129
     * @param string $tag    The tag to find, use : to delineate subtags
3130
     * @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
3131
     * @param string $gedrec The gedcom record to get the value from
3132
     *
3133
     * @return string the value of a gedcom tag from the given gedcom record
3134
     */
3135
    private function getGedcomValue(string $tag, int $level, string $gedrec): string
3136
    {
3137
        if ($gedrec === '') {
3138
            return '';
3139
        }
3140
        $tags      = explode(':', $tag);
3141
        $origlevel = $level;
3142
        if ($level === 0) {
3143
            $level = $gedrec[0] + 1;
3144
        }
3145
3146
        $subrec = $gedrec;
3147
        $t = 'XXXX';
3148
        foreach ($tags as $t) {
3149
            $lastsubrec = $subrec;
3150
            $subrec     = self::getSubRecord($level, "$level $t", $subrec);
3151
            if (empty($subrec) && $origlevel == 0) {
3152
                $level--;
3153
                $subrec = self::getSubRecord($level, "$level $t", $lastsubrec);
3154
            }
3155
            if (empty($subrec)) {
3156
                if ($t === 'TITL') {
3157
                    $subrec = self::getSubRecord($level, "$level ABBR", $lastsubrec);
3158
                    if (!empty($subrec)) {
3159
                        $t = 'ABBR';
3160
                    }
3161
                }
3162
                if ($subrec === '') {
3163
                    if ($level > 0) {
3164
                        $level--;
3165
                    }
3166
                    $subrec = self::getSubRecord($level, "@ $t", $gedrec);
3167
                    if ($subrec === '') {
3168
                        return '';
3169
                    }
3170
                }
3171
            }
3172
            $level++;
3173
        }
3174
        $level--;
3175
        $ct = preg_match("/$level $t(.*)/", $subrec, $match);
3176
        if ($ct === 0) {
3177
            $ct = preg_match("/$level @.+@ (.+)/", $subrec, $match);
3178
        }
3179
        if ($ct === 0) {
3180
            $ct = preg_match("/@ $t (.+)/", $subrec, $match);
3181
        }
3182
        if ($ct > 0) {
3183
            $value = trim($match[1]);
3184
            if ($t === 'NOTE' && preg_match('/^@(.+)@$/', $value, $match)) {
3185
                $note = Registry::noteFactory()->make($match[1], $this->tree);
3186
                if ($note instanceof Note) {
3187
                    $value = $note->getNote();
3188
                } else {
3189
                    //-- set the value to the id without the @
3190
                    $value = $match[1];
3191
                }
3192
            }
3193
            if ($level !== 0 || $t !== 'NOTE') {
3194
                $value .= self::getCont($level + 1, $subrec);
3195
            }
3196
3197
            if ($tag === 'NAME' || $tag === '_MARNM' || $tag === '_AKA') {
3198
                return strtr($value, ['/' => '']);
3199
            }
3200
3201
            if ($tag === 'NAME' || $tag === '_MARNM' || $tag === '_AKA') {
3202
                return strtr($value, ['/' => '']);
3203
            }
3204
3205
            return $value;
3206
        }
3207
3208
        return '';
3209
    }
3210
3211
    /**
3212
     * Replace variable identifiers with their values.
3213
     *
3214
     * @param string $expression An expression such as "$foo == 123"
3215
     * @param bool   $quote      Whether to add quotation marks
3216
     *
3217
     * @return string
3218
     */
3219
    private function substituteVars($expression, $quote): string
3220
    {
3221
        return preg_replace_callback(
3222
            '/\$(\w+)/',
3223
            function (array $matches) use ($quote): string {
3224
                if (isset($this->vars[$matches[1]]['id'])) {
3225
                    if ($quote) {
3226
                        return "'" . addcslashes($this->vars[$matches[1]]['id'], "'") . "'";
3227
                    }
3228
3229
                    return $this->vars[$matches[1]]['id'];
3230
                }
3231
3232
                Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1]));
3233
3234
                return '$' . $matches[1];
3235
            },
3236
            $expression
3237
        );
3238
    }
3239
}
3240