Passed
Pull Request — main (#4945)
by
unknown
05:54
created

ReportParserGenerate::factsStartHandler()   F

Complexity

Conditions 37
Paths > 20000

Size

Total Lines 135
Code Lines 91

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 37
eloc 91
nc 1955521
nop 1
dl 0
loc 135
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

1031
                    $ix1 = strpos(/** @scrutinizer ignore-type */ $name, '</span>', $ix1);
Loading history...
1032
                    $name = substr_replace($name, '»', $ix1, 7);
1033
                }
1034
                $addname = strip_tags((string) $tmp[0]['surn']);
1035
                if (!empty($addname) && !($addname === '@N.N.') && !str_contains($name, $addname)) {
1036
                    $name .= " " . I18N::translate('b.') . " " . $addname;
1037
                }
1038
                $this->current_element->addText(trim($name));
1039
            } else {
1040
                $name = $record->fullName();
1041
                $name = strip_tags($name);
1042
                if (!empty($attrs['truncate'])) {
1043
                    if ((int) $attrs['truncate'] > 0) {
1044
                        $name = Str::limit($name, (int) $attrs['truncate'], I18N::translate('…'));
1045
                    }
1046
                } else {
1047
                    $addname = (string) $record->alternateName();
1048
                    $addname = strip_tags($addname);
1049
                    if (!empty($addname)) {
1050
                        $name .= ' ' . $addname;
1051
                    }
1052
                }
1053
                $this->current_element->addText(trim($name));
1054
            }
1055
        }
1056
        if ($famrel && ($this->mfrelation[$record->xref()] != "")) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $record does not seem to be defined for all execution paths leading up to this point.
Loading history...
1057
            $this->current_element->addText(" (" . (string) $this->mfrelation[$record->xref()] . ")");
1058
        }
1059
    }
1060
1061
    /**
1062
     * Handle <gedcomValue />
1063
     *
1064
     * @param array<string> $attrs
1065
     *
1066
     * @return void
1067
     */
1068
    protected function gedcomValueStartHandler(array $attrs): void
1069
    {
1070
        $id    = '';
1071
        $match = [];
1072
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1073
            $id = $match[1];
1074
        }
1075
1076
        if (isset($attrs['newline']) && $attrs['newline'] === '1') {
1077
            $useBreak = '1';
1078
        } else {
1079
            $useBreak = '0';
1080
        }
1081
1082
        $tag = $attrs['tag'];
1083
        if (!empty($tag)) {
1084
            if ($tag === '@desc') {
1085
                $value = $this->desc;
1086
                $value = trim($value);
1087
                $this->current_element->addText($value);
1088
            }
1089
            if ($tag === '@id') {
1090
                $this->current_element->addText($id);
1091
            } else {
1092
                $tag = str_replace('@fact', $this->fact, $tag);
1093
                if (empty($attrs['level'])) {
1094
                    $level = (int) explode(' ', trim($this->gedrec))[0];
1095
                    if ($level === 0) {
1096
                        $level++;
1097
                    }
1098
                } else {
1099
                    $level = (int) $attrs['level'];
1100
                }
1101
                $tags  = preg_split('/[: ]/', $tag);
1102
                $value = $this->getGedcomValue($tag, $level, $this->gedrec);
1103
                switch (end($tags)) {
1104
                    case 'DATE':
1105
                        $tmp   = new Date($value);
1106
                        $dfmt = "%j %F %Y";
1107
                        if (!empty($attrs['truncate'])) {
1108
                            if ($attrs['truncate'] === "d") {
1109
                                $dfmt = "%j %M %Y";
1110
                            }
1111
                            if ($attrs['truncate'] === "Y") {
1112
                                $dfmt = "%Y";
1113
                            }
1114
                        }
1115
                        $value = strip_tags($tmp->display(null, $dfmt));
1116
                        break;
1117
                    case 'PLAC':
1118
                        $tmp   = new Place($value, $this->tree);
1119
                        $value = $tmp->shortName();
1120
                        break;
1121
                }
1122
                if ($useBreak === '1') {
1123
                    // Insert <br> when multiple dates exist.
1124
                    // This works around a TCPDF bug that incorrectly wraps RTL dates on LTR pages
1125
                    $value = str_replace('(', '<br>(', $value);
1126
                    $value = str_replace('<span dir="ltr"><br>', '<br><span dir="ltr">', $value);
1127
                    $value = str_replace('<span dir="rtl"><br>', '<br><span dir="rtl">', $value);
1128
                    if (substr($value, 0, 4) === '<br>') {
1129
                        $value = substr($value, 4);
1130
                    }
1131
                }
1132
                $tmp = explode(':', $tag);
1133
                if (in_array(end($tmp), ['NOTE', 'TEXT'], true)) {
1134
                    if ($this->tree->getPreference('FORMAT_TEXT') === 'xxmarkdown') {
1135
                        $value = strip_tags(Registry::markdownFactory()->markdown($value, $this->tree), ['br']);
1136
                    } else {
1137
                        $value = str_replace("\n", "<br>", $value);
1138
                        //$value = strip_tags(Registry::markdownFactory()->autolink($value, $this->tree), ['br']);
1139
                    }
1140
                    $value = strtr($value, [MarkdownFactory::BREAK => ' ']);
1141
                }
1142
1143
                if (isset($attrs['lcfirst'])) {
1144
                    $value = lcfirst($value);
1145
                    $value = str_replace(["Å","Ä","Ö"], ["å","ä","ö"], $value);
1146
                }
1147
1148
                if (!empty($attrs['truncate'])) {
1149
                    $value = strip_tags($value);
1150
                    if ((int) $attrs['truncate'] > 0) {
1151
                        $value = Str::limit($value, (int) $attrs['truncate'], I18N::translate('…'));
1152
                    }
1153
                }
1154
                $this->current_element->addText($value);
1155
            }
1156
        }
1157
    }
1158
1159
    /**
1160
     * Handle <repeatTag>
1161
     *
1162
     * @param array<string> $attrs
1163
     *
1164
     * @return void
1165
     */
1166
    protected function repeatTagStartHandler(array $attrs): void
1167
    {
1168
        $this->process_repeats++;
1169
        if ($this->process_repeats > 1) {
1170
            return;
1171
        }
1172
1173
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
1174
        $this->repeats         = [];
1175
        $this->repeat_bytes    = xml_get_current_line_number($this->parser);
1176
1177
        $tag = $attrs['tag'] ?? '';
1178
        if (!empty($tag)) {
1179
            if ($tag === '@desc') {
1180
                $value = $this->desc;
1181
                $value = trim($value);
1182
                $this->current_element->addText($value);
1183
            } else {
1184
                $tag   = str_replace('@fact', $this->fact, $tag);
1185
                $tags  = explode(':', $tag);
1186
                $level = (int) explode(' ', trim($this->gedrec))[0];
1187
                if ($level === 0) {
1188
                    $level++;
1189
                }
1190
                $subrec = $this->gedrec;
1191
                $t      = $tag;
1192
                $count  = count($tags);
1193
                $i      = 0;
1194
                while ($i < $count) {
1195
                    $t = $tags[$i];
1196
                    if (!empty($t)) {
1197
                        if ($i < ($count - 1)) {
1198
                            $subrec = self::getSubRecord($level, "$level $t", $subrec);
1199
                            if (empty($subrec)) {
1200
                                $level--;
1201
                                $subrec = self::getSubRecord($level, "@ $t", $this->gedrec);
1202
                                if (empty($subrec)) {
1203
                                    return;
1204
                                }
1205
                            }
1206
                        }
1207
                        $level++;
1208
                    }
1209
                    $i++;
1210
                }
1211
                $level--;
1212
                $count = preg_match_all("/$level $t(.*)/", $subrec, $match, PREG_SET_ORDER);
1213
                $i     = 0;
1214
                while ($i < $count) {
1215
                    $i++;
1216
                    // Privacy check - is this a link, and are we allowed to view the linked object?
1217
                    $subrecord = self::getSubRecord($level, "$level $t", $subrec, $i);
1218
                    if (preg_match('/^\d ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@/', $subrecord, $xref_match)) {
1219
                        $linked_object = Registry::gedcomRecordFactory()->make($xref_match[1], $this->tree);
1220
                        if ($linked_object && !$linked_object->canShow()) {
1221
                            //continue;
1222
                        }
1223
                    }
1224
                    $this->repeats[] = $subrecord;
1225
                }
1226
            }
1227
        }
1228
    }
1229
1230
    /**
1231
     * Handle </repeatTag>
1232
     *
1233
     * @return void
1234
     */
1235
    protected function repeatTagEndHandler(): void
1236
    {
1237
        $this->process_repeats--;
1238
        if ($this->process_repeats > 0) {
1239
            return;
1240
        }
1241
1242
        $nnnn = count($this->repeats);
0 ignored issues
show
Unused Code introduced by
The assignment to $nnnn is dead and can be removed.
Loading history...
1243
        $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...
1244
        // Check if there is anything to repeat
1245
        if (count($this->repeats) > 0) {
1246
            // No need to load them if not used...
1247
1248
            //-- read the xml from the file
1249
            $lines = file($this->report);
1250
            $lineoffset = 0;
1251
            foreach ($this->repeats_stack as $rep) {
1252
                $lineoffset += $rep[1] - 1;
1253
            }
1254
            while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<RepeatTag')) {
1255
                $lineoffset--;
1256
            }
1257
            $lineoffset++;
1258
            $reportxml = "<tempdoc>\n";
1259
            $line_nr   = $lineoffset + $this->repeat_bytes;
1260
            $lnnn = $line_nr;
0 ignored issues
show
Unused Code introduced by
The assignment to $lnnn is dead and can be removed.
Loading history...
1261
            // RepeatTag Level counter
1262
            $count = 1;
1263
            while (0 < $count) {
1264
                if (str_contains($lines[$line_nr], '<RepeatTag')) {
1265
                    $count++;
1266
                } elseif (str_contains($lines[$line_nr], '</RepeatTag')) {
1267
                    $count--;
1268
                }
1269
                if (0 < $count) {
1270
                    $reportxml .= $lines[$line_nr];
1271
                }
1272
                $line_nr++;
1273
            }
1274
            // No need to drag this
1275
            unset($lines);
1276
            $reportxml .= "</tempdoc>\n";
1277
            // Save original values
1278
            $this->parser_stack[] = $this->parser;
1279
            $oldgedrec            = $this->gedrec;
1280
            foreach ($this->repeats as $gedrec) {
1281
                $this->gedrec  = $gedrec;
1282
                $repeat_parser = xml_parser_create();
1283
                $this->parser  = $repeat_parser;
1284
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
1285
1286
                xml_set_element_handler(
1287
                    $repeat_parser,
1288
                    function ($parser, string $name, array $attrs): void {
1289
                        $this->startElement($parser, $name, $attrs);
1290
                    },
1291
                    function ($parser, string $name): void {
1292
                        $this->endElement($parser, $name);
1293
                    }
1294
                );
1295
1296
                xml_set_character_data_handler(
1297
                    $repeat_parser,
1298
                    function ($parser, string $data): void {
1299
                        $this->characterData($parser, $data);
1300
                    }
1301
                );
1302
1303
                if (!xml_parse($repeat_parser, $reportxml, true)) {
1304
                    throw new DomainException(sprintf(
1305
                        'RepeatTagEHandler XML error: %s at line %d',
1306
                        xml_error_string(xml_get_error_code($repeat_parser)),
1307
                        xml_get_current_line_number($repeat_parser)
1308
                    ));
1309
                }
1310
                xml_parser_free($repeat_parser);
1311
            }
1312
            // Restore original values
1313
            $this->gedrec = $oldgedrec;
1314
            $this->parser = array_pop($this->parser_stack);
1315
        }
1316
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
1317
    }
1318
1319
    /**
1320
     * Variable lookup
1321
     * Retrieve predefined variables :
1322
     * @ desc GEDCOM fact description, example:
1323
     *        1 EVEN This is a description
1324
     * @ fact GEDCOM fact tag, such as BIRT, DEAT etc.
1325
     * $ I18N::translate('....')
1326
     * $ language_settings[]
1327
     *
1328
     * @param array<string> $attrs an array of key value pairs for the attributes
1329
     *
1330
     * @return void
1331
     */
1332
    protected function varStartHandler(array $attrs): void
1333
    {
1334
        if (!isset($attrs['var'])) {
1335
            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));
1336
        }
1337
1338
        $var = $attrs['var'];
1339
        // SetVar element preset variables
1340
        if (!empty($this->vars[$var]['id'])) {
1341
            $var = $this->vars[$var]['id'];
1342
        } else {
1343
            $tfact = $this->fact;
1344
            if (($this->fact === 'EVEN' || $this->fact === 'FACT') && $this->type !== '') {
1345
                // Use :
1346
                // n TYPE This text if string
1347
                $tfact = $this->type;
1348
            } else {
1349
                foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) {
1350
                    $element = Registry::elementFactory()->make($record_type . ':' . $this->fact);
1351
1352
                    if (!$element instanceof UnknownElement) {
1353
                        $tfact = $element->label();
1354
                        break;
1355
                    }
1356
                }
1357
            }
1358
1359
            $var = strtr($var, ['@desc' => $this->desc, '@fact' => $tfact]);
1360
1361
            if (preg_match('/^I18N::number\((.+)\)$/', $var, $match)) {
1362
                $var = I18N::number((int) $match[1]);
1363
            } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $var, $match)) {
1364
                $var = I18N::translate($match[1]);
1365
            } elseif (preg_match('/^I18N::translate\(\$(.+)\)$/', $var, $match)) {
1366
                $var = I18N::translate($this->vars[$match[1]]['id']);
1367
            } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $var, $match)) {
1368
                $var = I18N::translateContext($match[1], $match[2]);
1369
            }
1370
        }
1371
        // Check if variable is set as a date and reformat the date
1372
        if (isset($attrs['date'])) {
1373
            if ($attrs['date'] === '1') {
1374
                $g   = new Date($var);
1375
                $var = $g->display();
1376
            }
1377
        }
1378
        if (isset($attrs['amp'])) {
1379
            $var = str_replace("%26", '&', $var);
1380
        }
1381
        if (isset($attrs['cut'])) {
1382
            $cut = (int) $attrs['cut'];
1383
            $var = $cut > 0 ? substr($var, 0, $cut) : substr($var, $cut);
1384
            if ($cut == 0) {
1385
                $var = "";
1386
            }
1387
        }
1388
        if (isset($attrs['lcfirst'])) {
1389
            $var = lcfirst($var);
1390
        }
1391
        $this->current_element->addText($var);
1392
        $this->text = $var; // Used for title/description
1393
    }
1394
1395
    /**
1396
     * Handle <facts>
1397
     *
1398
     * @param array<string> $attrs
1399
     *
1400
     * @return void
1401
     */
1402
    protected function factsStartHandler(array $attrs): void
1403
    {
1404
        $this->process_repeats++;
1405
        if ($this->process_repeats > 1) {
1406
            return;
1407
        }
1408
1409
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
1410
        $this->repeats         = [];
1411
        $this->repeat_bytes    = xml_get_current_line_number($this->parser);
1412
1413
        $id    = '';
1414
        $match = [];
1415
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1416
            $id = $match[1];
1417
        }
1418
        $tag = '';
1419
        if (isset($attrs['ignore'])) {
1420
            $tag .= $attrs['ignore'];
1421
        }
1422
        if (preg_match('/\$(.+)/', $tag, $match)) {
1423
            $tag = $this->vars[$match[1]]['id'];
1424
        }
1425
1426
        $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1427
        if (empty($attrs['diff']) && !empty($id)) {
1428
            $facts = $record->facts([], true);
1429
            $this->repeats = [];
1430
            $nonfacts      = explode(',', $tag);
1431
            foreach ($facts as $fact) {
1432
                $tag = explode(':', $fact->tag())[1];
1433
1434
                if (!in_array($tag, $nonfacts, true)) {
1435
                    $this->repeats[] = $fact->gedcom();
1436
                }
1437
            }
1438
        } else {
1439
            foreach ($record->facts() as $fact) {
1440
                if (($fact->isPendingAddition() || $fact->isPendingDeletion()) && !str_ends_with($fact->tag(), ':CHAN')) {
1441
                    $this->repeats[] = $fact->gedcom();
1442
                }
1443
            }
1444
        }
1445
1446
        // Add fact/event for FAM:DIV and for death of spouse
1447
        foreach ($this->repeats as $key => $fact) {
1448
            $jdarr[$key] = 0;
1449
            if (preg_match('/1 FAMS @(.+)@/', $fact, $match)) {
1450
                $famid = $match[1];
1451
                $fam = Registry::familyFactory()->make($match[1], $this->tree);
1452
                $dt = $this->getGedcomValue("MARR:DATE", 0, $fam->gedcom());
1453
                if ($dt == "") {
1454
                    $dt = $this->getGedcomValue("ENGA:DATE", 0, $fam->gedcom());
1455
                }
1456
                if ($dt == "" && $this->getGedcomValue("EVEN:TYPE", 0, $fam->gedcom()) == "Sambo") {
1457
                    $dt = $this->getGedcomValue("EVEN:DATE", 0, $fam->gedcom());
1458
                }
1459
                $date = new Date($dt);
1460
                $jd = $date->julianDay();
1461
                $jdarr[$key] = $jd;
1462
                // Divorce
1463
                $dt = $this->getGedcomValue("DIV:DATE", 0, $fam->gedcom());
1464
                if ($dt != "") {
1465
                    $this->repeats[] = "1 DIV\n2 DATE " . $dt . "\n";
1466
                }
1467
                // Separation // Doesn't work!! getGedComValue only reports the first event!! I.e. no match here
1468
                if ($this->getGedcomValue("EVEN:TYPE", 0, $fam->gedcom()) == "Separation") {
1469
                    $dt = $this->getGedcomValue("EVEN:DATE", 0, $fam->gedcom());
1470
                    if ($dt != "") {
1471
                        $this->repeats[] = "1 EVEN\n2 TYPE Separation\n2 DATE " . $dt . "\n";
1472
                    }
1473
                }
1474
                // death of husband / wife
1475
                $husb = $fam->husband();
1476
                $wife = $fam->wife();
1477
                if ($this->getGedcomValue("SEX", 0, $this->gedrec) == "M") {
1478
                    $spouse = $wife;
1479
                } else {
1480
                    $spouse = $husb;
1481
                }
1482
                if ($spouse) {
1483
                    $dt = $this->getGedcomValue("DEAT:DATE", 0, $spouse->gedcom());
1484
                } else {
1485
                    $dt = "";
1486
                }
1487
                if ($dt != "") {
1488
                    $this->repeats[] = "1 _SP_DEAT\n2 DATE " . $dt . "\n2 _O_FAM " . $famid . "\n";
1489
                }
1490
            }
1491
        }
1492
        // Find the dates for the facts that are found
1493
        foreach ($this->repeats as $key => $fact) {
1494
            if (preg_match('/[234] DATE ([^\n]+)/', $fact, $match)) {
1495
                $date = new Date($match[1]);
1496
                $jd = $date->julianDay();
1497
                $jdarr[$key] = $jd;
1498
            }
1499
        }
1500
1501
        // Resort facts in chronological order, if possible
1502
        $m = count($this->repeats) - 1;
1503
        $prevd = 0;
1504
        for ($i = 0; $i <= $m; $i++) { // keep undated events after previous dated event
1505
            if ($jdarr[$i] === 0) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $jdarr seems to be defined by a foreach iteration on line 1447. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
1506
                $jdarr[$i] = $prevd;
1507
            } else {
1508
                $prevd = $jdarr[$i];
1509
            }
1510
        }
1511
1512
        while ($m > 1) {
1513
            $n = count($this->repeats);
1514
            while ($n > 1) {
1515
                if ($jdarr[$n - 2] > $jdarr[$n - 1] && $jdarr[$n - 1] !== 0) {
1516
                    $s = $this->repeats[$n - 1];
1517
                    $this->repeats[$n - 1] = $this->repeats[$n - 2];
1518
                    $this->repeats[$n - 2] = $s;
1519
                    $s = $jdarr[$n - 1];
1520
                    $jdarr[$n - 1] = $jdarr[$n - 2];
1521
                    $jdarr[$n - 2] = $s;
1522
                }
1523
                $n -= 1;
1524
            }
1525
            $m -= 1;
1526
        }
1527
1528
        // Remove spouse deaths that are too late: after new marriage or own death
1529
        $currfam = "";
1530
        for ($i = 0; $i <= count($this->repeats) - 1; $i++) {
1531
            if (preg_match('/[1234] FAMS @(.+)@/', $this->repeats[$i], $match)) {
1532
                $currfam = $match[1];
1533
            }
1534
            if (preg_match('/_SP_DEAT.*\n2 DATE (.*)\n.*_O_FAM (.+)\n/', $this->repeats[$i], $match)) {
1535
                if ($currfam != $match[2] || $i == count($this->repeats) - 1) {
1536
                    $this->repeats[$i] = "1 _XXX\n";
1537
                } // ignore fact
1538
            }
1539
        }
1540
    }
1541
1542
    /**
1543
     * Handle </facts>
1544
     *
1545
     * @return void
1546
     */
1547
    protected function factsEndHandler(): void
1548
    {
1549
        $this->process_repeats--;
1550
        if ($this->process_repeats > 0) {
1551
            return;
1552
        }
1553
1554
        // Check if there is anything to repeat
1555
        if (count($this->repeats) > 0) {
1556
            $line       = xml_get_current_line_number($this->parser) - 1;
1557
            $lineoffset = 0;
1558
            foreach ($this->repeats_stack as $rep) {
1559
                $lineoffset += $rep[1] - 1;
1560
            }
1561
1562
            //-- read the xml from the file
1563
            $lines = file($this->report);
1564
            while ($lineoffset + $this->repeat_bytes > 0 && !str_contains($lines[$lineoffset + $this->repeat_bytes], '<Facts ')) {
1565
                $lineoffset--;
1566
            }
1567
            $lineoffset++;
1568
            $reportxml = "<tempdoc>\n";
1569
            $i         = $line + $lineoffset;
1570
            $line_nr   = $this->repeat_bytes + $lineoffset;
1571
            while ($line_nr < $i) {
1572
                $reportxml .= $lines[$line_nr];
1573
                $line_nr++;
1574
            }
1575
            // No need to drag this
1576
            unset($lines);
1577
            $reportxml .= "</tempdoc>\n";
1578
            // Save original values
1579
            $this->parser_stack[] = $this->parser;
1580
            $oldgedrec = $this->gedrec;
1581
            $count = count($this->repeats);
1582
            $i = 0;
1583
            while ($i < $count) {
1584
                if (!isset($this->repeats[$i])) {
1585
                    $i++;
1586
                    continue; // this fact has been removed above, occured too late
1587
                }
1588
                $this->gedrec = $this->repeats[$i];
1589
                $this->fact = '';
1590
                $this->desc = '';
1591
                if (preg_match('/1 (\w+)(.*)/', $this->gedrec, $match)) {
1592
                    $this->fact = $match[1];
1593
                    if ($this->fact === 'EVEN' || $this->fact === 'FACT') {
1594
                        $tmatch = [];
1595
                        if (preg_match('/2 TYPE (.+)/', $this->gedrec, $tmatch)) {
1596
                            $this->type = trim($tmatch[1]);
1597
                        } else {
1598
                            $this->type = ' ';
1599
                        }
1600
                    }
1601
                    $this->desc = trim($match[2]);
1602
                    $this->desc .= self::getCont(2, $this->gedrec);
1603
                }
1604
                $repeat_parser = xml_parser_create();
1605
                $this->parser  = $repeat_parser;
1606
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
1607
1608
                xml_set_element_handler(
1609
                    $repeat_parser,
1610
                    function ($parser, string $name, array $attrs): void {
1611
                        $this->startElement($parser, $name, $attrs);
1612
                    },
1613
                    function ($parser, string $name): void {
1614
                        $this->endElement($parser, $name);
1615
                    }
1616
                );
1617
1618
                xml_set_character_data_handler(
1619
                    $repeat_parser,
1620
                    function ($parser, string $data): void {
1621
                        $this->characterData($parser, $data);
1622
                    }
1623
                );
1624
1625
                if (!xml_parse($repeat_parser, $reportxml, true)) {
1626
                    throw new DomainException(sprintf(
1627
                        'FactsEHandler XML error: %s at line %d',
1628
                        xml_error_string(xml_get_error_code($repeat_parser)),
1629
                        xml_get_current_line_number($repeat_parser)
1630
                    ));
1631
                }
1632
                xml_parser_free($repeat_parser);
1633
                $i++;
1634
            }
1635
            // Restore original values
1636
            $this->parser = array_pop($this->parser_stack);
1637
            $this->gedrec = $oldgedrec;
1638
        }
1639
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
1640
    }
1641
1642
    /**
1643
     * Setting upp or changing variables in the XML
1644
     * The XML variable name and value is stored in $this->vars
1645
     *
1646
     * @param array<string> $attrs an array of key value pairs for the attributes
1647
     *
1648
     * @return void
1649
     */
1650
    protected function setVarStartHandler(array $attrs): void
1651
    {
1652
        if (empty($attrs['name'])) {
1653
            throw new DomainException('REPORT ERROR var: The attribute "name" is missing or not set in the XML file');
1654
        }
1655
1656
        $name  = $attrs['name'];
1657
        $value = $attrs['value'];
1658
        if (isset($attrs['dumpvar'])) {
1659
            $dumpvar = $attrs['dumpvar'];
1660
        } else {
1661
            $dumpvar = "";
1662
        }
1663
        $match = [];
1664
        // Current GEDCOM record strings
1665
        if ($value === '@ID') {
1666
            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1667
                $value = $match[1];
1668
            }
1669
        } elseif ($value === '@fact') {
1670
            $value = $this->fact;
1671
        } elseif ($value === '@desc') {
1672
            $value = $this->desc;
1673
        } elseif ($value === '@format') {
1674
            if (isset($_GET["format"])) {
1675
                $value = $_GET["format"];
1676
            } else {
1677
                $value = "";
1678
            }
1679
        } elseif ($value === '@generation') {
1680
            $value = (string) $this->generation;
1681
        } elseif ($value === '@base_url') {
1682
            $value = (string) $_SERVER["HTTP_REFERER"];
1683
            $i = strpos($value, "%2Freport%2F");
1684
            if ($i === false) {
1685
                $i = strpos($value, "/report/");
1686
            }
1687
            if ($i !== false) {
1688
                $value = substr($value, 0, $i);
1689
            }
1690
        } elseif ($value === '@relation') {
1691
            if (isset($this->mfrelation[$this->xref()])) {
0 ignored issues
show
Bug introduced by
The method xref() does not exist on Fisharebest\Webtrees\Report\ReportParserGenerate. ( Ignorable by Annotation )

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

1691
            if (isset($this->mfrelation[$this->/** @scrutinizer ignore-call */ xref()])) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1692
                $value = (string) $this->mfrelation[$this->xref()];
1693
            } else {
1694
                $value = "";
1695
            }
1696
        } elseif (preg_match("/@(\w+)/", $value, $match)) {
1697
            $gmatch = [];
1698
            if (preg_match("/\d $match[1] (.+)/", $this->gedrec, $gmatch)) {
1699
                $value = str_replace('@', '', trim($gmatch[1]));
1700
            }
1701
        } elseif (preg_match("/@\\$(\w+)/", $value, $match)) {
1702
            if ($match[1] == "dump" && $this->vars['dval']['id'] > 0) {
1703
                // if ($this->vars[ 'dval' ]['id'] == 1001)
1704
                if ($dumpvar == "gedrec") {
1705
                    error_log("\n---- setvar start  " . date("Y-m-d H:i:s") . " RPG " . __LINE__ . "  " . $name . "  gedcom=\n" . $this->gedrec . "\n", 3, "my-errors.log");
1706
                } elseif ($dumpvar != "") {
1707
                    error_log("var: " . $dumpvar . " = " . $this->vars[$dumpvar]['id'] . "\n", 3, "my-errors.log");
1708
                } else {
1709
                    if (isset($this->vars['dval']['id'])) {
1710
                        $nnn = $this->vars['dval']['id'];
1711
                    } else {
1712
                        $nnn = 0;
1713
                    }
1714
                    error_log("\n---- setvar start  " . date("Y-m-d H:i:s") . " RPG " . __LINE__ . "  " . $name . "  -----\n", 3, "my-errors.log");
1715
                    foreach ($this->vars as $key => $val) {
1716
                        if ($nnn-- < 0) {
1717
                            error_log($key . "='" . $val['id'] . "'\n", 3, "my-errors.log");
1718
                        }
1719
                    }
1720
                }
1721
            }
1722
            $value = $this->vars[$match[1]]['id'];
1723
            if (isset($this->vars[$value]['id'])) {
1724
                $value = '$' . $this->vars[$match[1]]['id'];
1725
            } else {
1726
                $value = "0";
1727
            }
1728
        }
1729
        if (isset($attrs['trim'])) {
1730
            $value = str_replace($attrs['trim'], '', $value);
1731
        }
1732
        if (preg_match("/\\$(\w+)/", $name, $match)) {
1733
            $name = $this->vars["'" . $match[1] . "'"]['id'];
1734
        }
1735
        $count = preg_match_all("/\\$(\w+)/", $value, $match, PREG_SET_ORDER);
1736
        $i     = 0;
1737
        while ($i < $count) {
1738
            $t     = $this->vars[$match[$i][1]]['id'];
1739
            $value = preg_replace('/\$' . $match[$i][1] . '/', $t, $value, 1);
1740
            $i++;
1741
        }
1742
        if (preg_match('/^I18N::number\((.+)\)$/', $value, $match)) {
1743
            $value = I18N::number((int) $match[1]);
1744
        } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $value, $match)) {
1745
            $value = I18N::translate($match[1]);
1746
        } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $value, $match)) {
1747
            $value = I18N::translateContext($match[1], $match[2]);
1748
        }
1749
        if (isset($attrs['lcfirst'])) { // set 1st char to lower case
1750
            $value = lcfirst($value);
1751
        }
1752
1753
        // Arithmetic functions
1754
        if (preg_match("/(\d+)\s*([-+*\/])\s*(\d+)/", $value, $match)) {
1755
            // Create an expression language with the functions used by our reports.
1756
            $expression_provider  = new ReportExpressionLanguageProvider();
1757
            $expression_cache     = new NullAdapter();
1758
            $expression_language  = new ExpressionLanguage($expression_cache, [$expression_provider]);
1759
1760
            $value = (string) $expression_language->evaluate($value);
1761
        }
1762
1763
        if (str_contains($value, '@')) {
1764
            $value = '';
1765
        }
1766
        $this->vars[$name]['id'] = $value;
1767
        if ($name == 'title') {
1768
            $this->wt_report->title = $value;
1769
        }
1770
    }
1771
1772
    /**
1773
     * Handle <if>
1774
     *
1775
     * @param array<string> $attrs
1776
     *
1777
     * @return void
1778
     */
1779
    protected function ifStartHandler(array $attrs): void
1780
    {
1781
        if ($this->process_ifs > 0) {
1782
            $this->process_ifs++;
1783
1784
            return;
1785
        }
1786
1787
        $condition = $attrs['condition'];
1788
        $condition = $this->substituteVars($condition, true);
1789
        $condition = str_replace([
1790
            ' LT ',
1791
            ' GT ',
1792
        ], [
1793
            '<',
1794
            '>',
1795
        ], $condition);
1796
        // Replace the first occurrence only once of @fact:DATE or in any other combinations to the current fact, such as BIRT
1797
        $condition = str_replace('@fact:', $this->fact . ':', $condition);
1798
        $match     = [];
1799
        $count     = preg_match_all("/@([\w:.]+)/", $condition, $match, PREG_SET_ORDER);
1800
        $i         = 0;
1801
        while ($i < $count) {
1802
            $id    = $match[$i][1];
1803
            $value = '""';
1804
            if ($id === 'ID') {
1805
                if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1806
                    $value = "'" . $match[1] . "'";
1807
                }
1808
            } elseif ($id === 'fact') {
1809
                $value = '"' . $this->fact . '"';
1810
            } elseif ($id === 'desc') {
1811
                $value = '"' . addslashes($this->desc) . '"';
1812
            } elseif ($id === 'generation') {
1813
                $value = '"' . $this->generation . '"';
1814
            } else {
1815
                $level = (int) explode(' ', trim($this->gedrec))[0];
1816
                if ($level === 0) {
1817
                    $level++;
1818
                }
1819
                $value = $this->getGedcomValue($id, $level, $this->gedrec);
1820
                if (empty($value)) {
1821
                    $level++;
1822
                    $value = $this->getGedcomValue($id, $level, $this->gedrec);
1823
                }
1824
                $value = preg_replace('/^@(' . Gedcom::REGEX_XREF . ')@$/', '$1', $value);
1825
                $value = '"' . addslashes($value) . '"';
1826
            }
1827
            $condition = str_replace("@$id", $value, $condition);
1828
            $i++;
1829
        }
1830
1831
        // Create an expression language with the functions used by our reports.
1832
        $expression_provider  = new ReportExpressionLanguageProvider();
1833
        $expression_cache     = new NullAdapter();
1834
        $expression_language  = new ExpressionLanguage($expression_cache, [$expression_provider]);
1835
1836
        $ret = $expression_language->evaluate($condition);
1837
1838
        if (!$ret) {
1839
            $this->process_ifs++;
1840
        }
1841
    }
1842
1843
    /**
1844
     * Handle </if>
1845
     *
1846
     * @return void
1847
     */
1848
    protected function ifEndHandler(): void
1849
    {
1850
        if ($this->process_ifs > 0) {
1851
            $this->process_ifs--;
1852
        }
1853
    }
1854
1855
    /**
1856
     * Handle <footnote>
1857
     * Collect the Footnote links
1858
     * GEDCOM Records that are protected by Privacy setting will be ignored
1859
     *
1860
     * @param array<string> $attrs
1861
     *
1862
     * @return void
1863
     */
1864
    protected function footnoteStartHandler(array $attrs): void
1865
    {
1866
        $id = '';
1867
        if (preg_match('/[0-9] (.+) @(.+)@/', $this->gedrec, $match)) {
1868
            $id = $match[2];
1869
        }
1870
        $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1871
        if ($record && $record->canShow()) {
1872
            $this->print_data_stack[] = $this->print_data;
1873
            $this->print_data         = true;
1874
            $style                    = '';
1875
            if (!empty($attrs['style'])) {
1876
                $style = $attrs['style'];
1877
            }
1878
            $this->footnote_element = $this->current_element;
1879
            $this->current_element  = $this->report_root->createFootnote($style);
1880
        } else {
1881
            $this->print_data       = false;
1882
            $this->process_footnote = false;
1883
        }
1884
    }
1885
1886
    /**
1887
     * Handle </footnote>
1888
     * Print the collected Footnote data
1889
     *
1890
     * @return void
1891
     */
1892
    protected function footnoteEndHandler(): void
1893
    {
1894
        if ($this->process_footnote) {
1895
            $this->print_data = array_pop($this->print_data_stack);
1896
            $temp             = trim($this->current_element->getValue());
1897
            if (strlen($temp) > 3) {
1898
                $this->wt_report->addElement($this->current_element);
1899
            }
1900
            $this->current_element = $this->footnote_element;
1901
        } else {
1902
            $this->process_footnote = true;
1903
        }
1904
    }
1905
1906
    /**
1907
     * Handle <footnoteTexts />
1908
     *
1909
     * @return void
1910
     */
1911
    protected function footnoteTextsStartHandler(): void
1912
    {
1913
        $temp = 'footnotetexts';
1914
        $this->wt_report->addElement($temp);
1915
    }
1916
1917
    /**
1918
     * XML element Forced line break handler - HTML code
1919
     *
1920
     * @return void
1921
     */
1922
    protected function brStartHandler(): void
1923
    {
1924
        if ($this->print_data && $this->process_gedcoms === 0) {
1925
            $this->current_element->addText('<br>');
1926
        }
1927
    }
1928
1929
    /**
1930
     * Handle <sp />
1931
     * Forced space
1932
     *
1933
     * @return void
1934
     */
1935
    protected function spStartHandler(): void
1936
    {
1937
        if ($this->print_data && $this->process_gedcoms === 0) {
1938
            $this->current_element->addText(' ');
1939
        }
1940
    }
1941
1942
    /**
1943
     * Handle <highlightedImage />
1944
     *
1945
     * @param array<string> $attrs
1946
     *
1947
     * @return void
1948
     */
1949
    protected function highlightedImageStartHandler(array $attrs): void
1950
    {
1951
        $id = '';
1952
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1953
            $id = $match[1];
1954
        }
1955
1956
        // Position the top corner of this box on the page
1957
        $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION);
1958
1959
        // Position the left corner of this box on the page
1960
        $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION);
1961
1962
        // string Align the image in left, center, right (or empty to use x/y position).
1963
        $align = $attrs['align'] ?? '';
1964
1965
        // string Next Line should be T:next to the image, N:next line
1966
        $ln = $attrs['ln'] ?? 'T';
1967
1968
        // Width, height (or both).
1969
        $width  = (float) ($attrs['width'] ?? 0.0);
1970
        $height = (float) ($attrs['height'] ?? 0.0);
1971
1972
        $person     = Registry::individualFactory()->make($id, $this->tree);
1973
        $media_file = $person->findHighlightedMediaFile();
1974
1975
        if ($media_file instanceof MediaFile && $media_file->fileExists()) {
1976
            $image      = imagecreatefromstring($media_file->fileContents());
1977
            $attributes = [imagesx($image), imagesy($image)];
1978
1979
            if ($width > 0 && $height == 0) {
1980
                $perc   = $width / $attributes[0];
1981
                $height = round($attributes[1] * $perc);
1982
            } elseif ($height > 0 && $width == 0) {
1983
                $perc  = $height / $attributes[1];
1984
                $width = round($attributes[0] * $perc);
1985
            } else {
1986
                $width  = (float) $attributes[0];
1987
                $height = (float) $attributes[1];
1988
            }
1989
            $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln);
1990
            $this->wt_report->addElement($image);
1991
        }
1992
    }
1993
1994
    /**
1995
     * Handle <image/>
1996
     *
1997
     * @param array<string> $attrs
1998
     *
1999
     * @return void
2000
     */
2001
    protected function imageStartHandler(array $attrs): void
2002
    {
2003
        // Position the top corner of this box on the page. the default is the current position
2004
        $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION);
2005
2006
        // mixed Position the left corner of this box on the page. the default is the current position
2007
        $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION);
2008
2009
        // string Align the image in left, center, right (or empty to use x/y position).
2010
        $align = $attrs['align'] ?? '';
2011
2012
        // string Next Line should be T:next to the image, N:next line
2013
        $ln = $attrs['ln'] ?? 'T';
2014
2015
        // Width, height (or both).
2016
        $width  = (float) ($attrs['width'] ?? 0.0);
2017
        $height = (float) ($attrs['height'] ?? 0.0);
2018
2019
        $file = $attrs['file'] ?? '';
2020
2021
        if ($file === '@FILE') {
2022
            $match = [];
2023
            if (preg_match("/\d OBJE @(.+)@/", $this->gedrec, $match)) {
2024
                $mediaobject = Registry::mediaFactory()->make($match[1], $this->tree);
2025
                $media_file  = $mediaobject->firstImageFile();
2026
2027
                if ($media_file instanceof MediaFile && $media_file->fileExists()) {
2028
                    $image      = imagecreatefromstring($media_file->fileContents());
2029
                    $attributes = [imagesx($image), imagesy($image)];
2030
2031
                    if ($width > 0 && $height == 0) {
2032
                        $perc   = $width / $attributes[0];
2033
                        $height = round($attributes[1] * $perc);
2034
                    } elseif ($height > 0 && $width == 0) {
2035
                        $perc  = $height / $attributes[1];
2036
                        $width = round($attributes[0] * $perc);
2037
                    } else {
2038
                        $width  = (float) $attributes[0];
2039
                        $height = (float) $attributes[1];
2040
                    }
2041
                    $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln);
2042
                    $this->wt_report->addElement($image);
2043
                }
2044
            }
2045
        } else {
2046
            if (file_exists($file) && preg_match('/(jpg|jpeg|png|gif)$/i', $file)) {
2047
                $size = getimagesize($file);
2048
                if ($width > 0 && $height == 0) {
2049
                    $perc   = $width / $size[0];
2050
                    $height = round($size[1] * $perc);
2051
                } elseif ($height > 0 && $width == 0) {
2052
                    $perc  = $height / $size[1];
2053
                    $width = round($size[0] * $perc);
2054
                } else {
2055
                    $width  = $size[0];
2056
                    $height = $size[1];
2057
                }
2058
                $image = $this->report_root->createImage($file, $left, $top, $width, $height, $align, $ln);
2059
                $this->wt_report->addElement($image);
2060
            }
2061
        }
2062
    }
2063
2064
    /**
2065
     * Handle <line>
2066
     *
2067
     * @param array<string> $attrs
2068
     *
2069
     * @return void
2070
     */
2071
    protected function lineStartHandler(array $attrs): void
2072
    {
2073
        // Start horizontal position, current position (default)
2074
        $x1 = ReportBaseElement::CURRENT_POSITION;
2075
        if (isset($attrs['x1'])) {
2076
            if ($attrs['x1'] === '0') {
2077
                $x1 = 0;
2078
            } elseif ($attrs['x1'] === '.') {
2079
                $x1 = ReportBaseElement::CURRENT_POSITION;
2080
            } elseif (!empty($attrs['x1'])) {
2081
                $x1 = (float) $attrs['x1'];
2082
            }
2083
        }
2084
        // Start vertical position, current position (default)
2085
        $y1 = ReportBaseElement::CURRENT_POSITION;
2086
        if (isset($attrs['y1'])) {
2087
            if ($attrs['y1'] === '0') {
2088
                $y1 = 0;
2089
            } elseif ($attrs['y1'] === '.') {
2090
                $y1 = ReportBaseElement::CURRENT_POSITION;
2091
            } elseif (!empty($attrs['y1'])) {
2092
                $y1 = (float) $attrs['y1'];
2093
            }
2094
        }
2095
        // End horizontal position, maximum width (default)
2096
        $x2 = ReportBaseElement::CURRENT_POSITION;
2097
        if (isset($attrs['x2'])) {
2098
            if ($attrs['x2'] === '0') {
2099
                $x2 = 0;
2100
            } elseif ($attrs['x2'] === '.') {
2101
                $x2 = ReportBaseElement::CURRENT_POSITION;
2102
            } elseif (!empty($attrs['x2'])) {
2103
                $x2 = (float) $attrs['x2'];
2104
            }
2105
        }
2106
        // End vertical position
2107
        $y2 = ReportBaseElement::CURRENT_POSITION;
2108
        if (isset($attrs['y2'])) {
2109
            if ($attrs['y2'] === '0') {
2110
                $y2 = 0;
2111
            } elseif ($attrs['y2'] === '.') {
2112
                $y2 = ReportBaseElement::CURRENT_POSITION;
2113
            } elseif (!empty($attrs['y2'])) {
2114
                $y2 = (float) $attrs['y2'];
2115
            }
2116
        }
2117
2118
        $line = $this->report_root->createLine($x1, $y1, $x2, $y2);
2119
        $this->wt_report->addElement($line);
2120
    }
2121
2122
    /**
2123
     * Handle <list>
2124
     *
2125
     * @param array<string> $attrs
2126
     *
2127
     * @return void
2128
     */
2129
    protected function listStartHandler(array $attrs): void
2130
    {
2131
        $this->process_repeats++;
2132
        if ($this->process_repeats > 1) {
2133
            return;
2134
        }
2135
2136
        $match = [];
2137
        if (isset($attrs['sortby'])) {
2138
            $sortby = $attrs['sortby'];
2139
            if (preg_match("/\\$(\w+)/", $sortby, $match)) {
2140
                $sortby = $this->vars[$match[1]]['id'];
2141
                $sortby = trim($sortby);
2142
            }
2143
        } else {
2144
            $sortby = 'NAME';
2145
        }
2146
2147
        $listname = $attrs['list'] ?? 'individual';
2148
2149
        // Some filters/sorts can be applied using SQL, while others require PHP
2150
        switch ($listname) {
2151
            case 'pending':
2152
                $this->list = DB::table('change')
2153
                    ->whereIn('change_id', function (Builder $query): void {
2154
                        $query->select([new Expression('MAX(change_id)')])
2155
                            ->from('change')
2156
                            ->where('gedcom_id', '=', $this->tree->id())
2157
                            ->where('status', '=', 'pending')
2158
                            ->groupBy(['xref']);
2159
                    })
2160
                    ->get()
2161
                    ->map(fn (object $row): ?GedcomRecord => Registry::gedcomRecordFactory()->make($row->xref, $this->tree, $row->new_gedcom ?: $row->old_gedcom))
2162
                    ->filter()
2163
                    ->all();
2164
                break;
2165
2166
            case 'individual':
2167
                $query = DB::table('individuals')
2168
                    ->where('i_file', '=', $this->tree->id())
2169
                    ->select(['i_id AS xref', 'i_gedcom AS gedcom'])
2170
                    ->distinct();
2171
2172
                foreach ($attrs as $attr => $value) {
2173
                    if (str_starts_with($attr, 'filter') && $value !== '') {
2174
                        $value = $this->substituteVars($value, false);
2175
                        // Convert the various filters into SQL
2176
                        if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
2177
                            $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2178
                                $join
2179
                                    ->on($attr . '.d_gid', '=', 'i_id')
2180
                                    ->on($attr . '.d_file', '=', 'i_file');
2181
                            });
2182
2183
                            $query->where($attr . '.d_fact', '=', $match[1]);
2184
2185
                            $date = new Date($match[3]);
2186
2187
                            if ($match[2] === 'LTE') {
2188
                                $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay());
2189
                            } else {
2190
                                $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay());
2191
                            }
2192
2193
                            // This filter has been fully processed
2194
                            unset($attrs[$attr]);
2195
                        } elseif (preg_match('/^NAME CONTAINS (.+)$/', $value, $match)) {
2196
                            $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2197
                                $join
2198
                                    ->on($attr . '.n_id', '=', 'i_id')
2199
                                    ->on($attr . '.n_file', '=', 'i_file');
2200
                            });
2201
                            // Search the DB only if there is any name supplied
2202
                            $names = explode(' ', $match[1]);
2203
                            foreach ($names as $name) {
2204
                                $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%');
2205
                            }
2206
2207
                            // This filter has been fully processed
2208
                            unset($attrs[$attr]);
2209
                        } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) {
2210
                            // Convert newline escape sequences to actual new lines
2211
                            $match[1] = str_replace('\n', "\n", $match[1]);
2212
2213
                            $query->where('i_gedcom', 'LIKE', $match[1]);
2214
2215
                            // This filter has been fully processed
2216
                            unset($attrs[$attr]);
2217
                        } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) {
2218
                            // Don't unset this filter. This is just initial filtering for performance
2219
                            $query
2220
                                ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void {
2221
                                    $join
2222
                                        ->on($attr . 'a.pl_file', '=', 'i_file')
2223
                                        ->on($attr . 'a.pl_gid', '=', 'i_id');
2224
                                })
2225
                                ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void {
2226
                                    $join
2227
                                        ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file')
2228
                                        ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id');
2229
                                })
2230
                                ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%');
2231
                        } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) {
2232
                            // Don't unset this filter. This is just initial filtering for performance
2233
                            $match[3] = strtr($match[3], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2234
                            $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%';
2235
                            $query->where('i_gedcom', 'LIKE', $like);
2236
                        } elseif (preg_match('/^(\w+) CONTAINS (.*)$/', $value, $match)) {
2237
                            // Don't unset this filter. This is just initial filtering for performance
2238
                            $match[2] = strtr($match[2], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2239
                            $like = "%\n1 " . $match[1] . '%' . $match[2] . '%';
2240
                            $query->where('i_gedcom', 'LIKE', $like);
2241
                        }
2242
                    }
2243
                }
2244
2245
                $this->list = [];
2246
2247
                foreach ($query->get() as $row) {
2248
                    $this->list[$row->xref] = Registry::individualFactory()->make($row->xref, $this->tree, $row->gedcom);
2249
                }
2250
                break;
2251
2252
            case 'family':
2253
                $query = DB::table('families')
2254
                    ->where('f_file', '=', $this->tree->id())
2255
                    ->select(['f_id AS xref', 'f_gedcom AS gedcom'])
2256
                    ->distinct();
2257
2258
                foreach ($attrs as $attr => $value) {
2259
                    if (str_starts_with($attr, 'filter') && $value !== '') {
2260
                        $value = $this->substituteVars($value, false);
2261
                        // Convert the various filters into SQL
2262
                        if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
2263
                            $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2264
                                $join
2265
                                    ->on($attr . '.d_gid', '=', 'f_id')
2266
                                    ->on($attr . '.d_file', '=', 'f_file');
2267
                            });
2268
2269
                            $query->where($attr . '.d_fact', '=', $match[1]);
2270
2271
                            $date = new Date($match[3]);
2272
2273
                            if ($match[2] === 'LTE') {
2274
                                $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay());
2275
                            } else {
2276
                                $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay());
2277
                            }
2278
2279
                            // This filter has been fully processed
2280
                            unset($attrs[$attr]);
2281
                        } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) {
2282
                            // Convert newline escape sequences to actual new lines
2283
                            $match[1] = str_replace('\n', "\n", $match[1]);
2284
2285
                            $query->where('f_gedcom', 'LIKE', $match[1]);
2286
2287
                            // This filter has been fully processed
2288
                            unset($attrs[$attr]);
2289
                        } elseif (preg_match('/^NAME CONTAINS (.*)$/', $value, $match)) {
2290
                            if ($sortby === 'NAME' || $match[1] !== '') {
2291
                                $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2292
                                    $join
2293
                                        ->on($attr . '.n_file', '=', 'f_file')
2294
                                        ->where(static function (Builder $query): void {
2295
                                            $query
2296
                                                ->whereColumn('n_id', '=', 'f_husb')
2297
                                                ->orWhereColumn('n_id', '=', 'f_wife');
2298
                                        });
2299
                                });
2300
                                // Search the DB only if there is any name supplied
2301
                                if ($match[1] != '') {
2302
                                    $names = explode(' ', $match[1]);
2303
                                    foreach ($names as $name) {
2304
                                        $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%');
2305
                                    }
2306
                                }
2307
                            }
2308
2309
                            // This filter has been fully processed
2310
                            unset($attrs[$attr]);
2311
                        } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) {
2312
                            // Don't unset this filter. This is just initial filtering for performance
2313
                            $query
2314
                                ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void {
2315
                                    $join
2316
                                        ->on($attr . 'a.pl_file', '=', 'f_file')
2317
                                        ->on($attr . 'a.pl_gid', '=', 'f_id');
2318
                                })
2319
                                ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void {
2320
                                    $join
2321
                                        ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file')
2322
                                        ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id');
2323
                                })
2324
                                ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%');
2325
                        } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) {
2326
                            // Don't unset this filter. This is just initial filtering for performance
2327
                            $match[3] = strtr($match[3], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2328
                            $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%';
2329
                            $query->where('f_gedcom', 'LIKE', $like);
2330
                        } elseif (preg_match('/^(\w+) CONTAINS (.+)$/', $value, $match)) {
2331
                            // Don't unset this filter. This is just initial filtering for performance
2332
                            $match[2] = strtr($match[2], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2333
                            $like = "%\n1 " . $match[1] . '%' . $match[2] . '%';
2334
                            $query->where('f_gedcom', 'LIKE', $like);
2335
                        }
2336
                    }
2337
                }
2338
2339
                $this->list = [];
2340
2341
                foreach ($query->get() as $row) {
2342
                    $this->list[$row->xref] = Registry::familyFactory()->make($row->xref, $this->tree, $row->gedcom);
2343
                }
2344
                break;
2345
2346
            default:
2347
                throw new DomainException('Invalid list name: ' . $listname);
2348
        }
2349
2350
        $filters  = [];
2351
        $filters2 = [];
2352
        if (isset($attrs['filter1']) && count($this->list) > 0) {
2353
            foreach ($attrs as $key => $value) {
2354
                if (preg_match("/filter(\d)/", $key)) {
2355
                    $condition = $value;
2356
                    if (preg_match("/@(\w+)/", $condition, $match)) {
2357
                        $id    = $match[1];
2358
                        $value = "''";
2359
                        if ($id === 'ID') {
2360
                            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
2361
                                $value = "'" . $match[1] . "'";
2362
                            }
2363
                        } elseif ($id === 'fact') {
2364
                            $value = "'" . $this->fact . "'";
2365
                        } elseif ($id === 'desc') {
2366
                            $value = "'" . $this->desc . "'";
2367
                        } else {
2368
                            if (preg_match("/\d $id (.+)/", $this->gedrec, $match)) {
2369
                                $value = "'" . str_replace('@', '', trim($match[1])) . "'";
2370
                            }
2371
                        }
2372
                        $condition = preg_replace("/@$id/", $value, $condition);
2373
                    }
2374
                    //-- handle regular expressions
2375
                    if (preg_match("/([A-Z:]+)\s*([^\s]+)\s*(.+)/", $condition, $match)) {
2376
                        $tag  = trim($match[1]);
2377
                        $expr = trim($match[2]);
2378
                        $val  = trim($match[3]);
2379
                        if (preg_match("/\\$(\w+)/", $val, $match)) {
2380
                            $val = $this->vars[$match[1]]['id'];
2381
                            $val = trim($val);
2382
                        }
2383
                        if ($val !== '') {
2384
                            $searchstr = '';
2385
                            $tags      = explode(':', $tag);
2386
                            //-- only limit to a level number if we are specifically looking at a level
2387
                            if (count($tags) > 1) {
2388
                                $level = 1;
2389
                                $t = 'XXXX';
2390
                                foreach ($tags as $t) {
2391
                                    if (!empty($searchstr)) {
2392
                                        $searchstr .= "[^\n]*(\n[2-9][^\n]*)*\n";
2393
                                    }
2394
                                    //-- search for both EMAIL and _EMAIL... silly double gedcom standard
2395
                                    if ($t === 'EMAIL' || $t === '_EMAIL') {
2396
                                        $t = '_?EMAIL';
2397
                                    }
2398
                                    $searchstr .= $level . ' ' . $t;
2399
                                    $level++;
2400
                                }
2401
                            } else {
2402
                                if ($tag === 'EMAIL' || $tag === '_EMAIL') {
2403
                                    $tag = '_?EMAIL';
2404
                                }
2405
                                $t         = $tag;
2406
                                $searchstr = '1 ' . $tag;
2407
                            }
2408
                            switch ($expr) {
2409
                                case 'CONTAINS':
2410
                                    if ($t === 'PLAC') {
2411
                                        $searchstr .= "[^\n]*[, ]*" . $val;
2412
                                    } else {
2413
                                        $searchstr .= "[^\n]*" . $val;
2414
                                    }
2415
                                    $filters[] = $searchstr;
2416
                                    break;
2417
                                default:
2418
                                    $filters2[] = [
2419
                                        'tag'  => $tag,
2420
                                        'expr' => $expr,
2421
                                        'val'  => $val,
2422
                                    ];
2423
                                    break;
2424
                            }
2425
                        }
2426
                    }
2427
                }
2428
            }
2429
        }
2430
        //-- apply other filters to the list that could not be added to the search string
2431
        if ($filters !== []) {
2432
            foreach ($this->list as $key => $record) {
2433
                foreach ($filters as $filter) {
2434
                    if (!preg_match('/' . $filter . '/i', $record->privatizeGedcom(Auth::accessLevel($this->tree)))) {
2435
                        unset($this->list[$key]);
2436
                        break;
2437
                    }
2438
                }
2439
            }
2440
        }
2441
        if ($filters2 !== []) {
2442
            $mylist = [];
2443
            foreach ($this->list as $indi) {
2444
                $key  = $indi->xref();
2445
                $grec = $indi->privatizeGedcom(Auth::accessLevel($this->tree));
2446
                $keep = true;
2447
                foreach ($filters2 as $filter) {
2448
                    if ($keep) {
2449
                        $tag  = $filter['tag'];
2450
                        $expr = $filter['expr'];
2451
                        $val  = $filter['val'];
2452
                        if ($val === "''") {
2453
                            $val = '';
2454
                        }
2455
                        $tags = explode(':', $tag);
2456
                        $t    = end($tags);
2457
                        $v    = $this->getGedcomValue($tag, 1, $grec);
2458
                        //-- check for EMAIL and _EMAIL (silly double gedcom standard :P)
2459
                        if ($t === 'EMAIL' && empty($v)) {
2460
                            $tag  = str_replace('EMAIL', '_EMAIL', $tag);
2461
                            $tags = explode(':', $tag);
2462
                            $t    = end($tags);
2463
                            $v    = self::getSubRecord(1, $tag, $grec);
2464
                        }
2465
2466
                        switch ($expr) {
2467
                            case 'GTE':
2468
                                if ($t === 'DATE') {
2469
                                    $date1 = new Date($v);
2470
                                    $date2 = new Date($val);
2471
                                    $keep  = (Date::compare($date1, $date2) >= 0);
2472
                                } elseif ($val >= $v) {
2473
                                    $keep = true;
2474
                                }
2475
                                break;
2476
                            case 'LTE':
2477
                                if ($t === 'DATE') {
2478
                                    $date1 = new Date($v);
2479
                                    $date2 = new Date($val);
2480
                                    $keep  = (Date::compare($date1, $date2) <= 0);
2481
                                } elseif ($val >= $v) {
2482
                                    $keep = true;
2483
                                }
2484
                                break;
2485
                            default:
2486
                                if ($v == $val) {
2487
                                    $keep = true;
2488
                                } else {
2489
                                    $keep = false;
2490
                                }
2491
                                break;
2492
                        }
2493
                    }
2494
                }
2495
                if ($keep) {
2496
                    $mylist[$key] = $indi;
2497
                }
2498
            }
2499
            $this->list = $mylist;
2500
        }
2501
2502
        switch ($sortby) {
2503
            case 'NAME':
2504
                uasort($this->list, GedcomRecord::nameComparator());
2505
                break;
2506
            case 'CHAN':
2507
                uasort($this->list, GedcomRecord::lastChangeComparator());
2508
                break;
2509
            case 'BIRT:DATE':
2510
                uasort($this->list, Individual::birthDateComparator());
2511
                break;
2512
            case 'DEAT:DATE':
2513
                uasort($this->list, Individual::deathDateComparator());
2514
                break;
2515
            case 'MARR:DATE':
2516
                uasort($this->list, Family::marriageDateComparator());
2517
                break;
2518
            default:
2519
                // unsorted or already sorted by SQL
2520
                break;
2521
        }
2522
2523
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
2524
        $this->repeat_bytes    = xml_get_current_line_number($this->parser) + 1;
2525
    }
2526
2527
    /**
2528
     * Handle </list>
2529
     *
2530
     * @return void
2531
     */
2532
    protected function listEndHandler(): void
2533
    {
2534
        $this->process_repeats--;
2535
        if ($this->process_repeats > 0) {
2536
            return;
2537
        }
2538
2539
        // Check if there is any list
2540
        if (count($this->list) > 0) {
2541
            $lineoffset = 0;
2542
            foreach ($this->repeats_stack as $rep) {
2543
                $lineoffset += $rep[1] - 1;
2544
            }
2545
            //-- read the xml from the file
2546
            $lines = file($this->report);
2547
            while ((!str_contains($lines[$lineoffset + $this->repeat_bytes], '<List')) && (($lineoffset + $this->repeat_bytes) > 0)) {
2548
                $lineoffset--;
2549
            }
2550
            $lineoffset++;
2551
            $reportxml = "<tempdoc>\n";
2552
            $line_nr   = $lineoffset + $this->repeat_bytes;
2553
            // List Level counter
2554
            $count = 1;
2555
            while (0 < $count) {
2556
                if (str_contains($lines[$line_nr], '<List')) {
2557
                    $count++;
2558
                } elseif (str_contains($lines[$line_nr], '</List')) {
2559
                    $count--;
2560
                }
2561
                if (0 < $count) {
2562
                    $reportxml .= $lines[$line_nr];
2563
                }
2564
                $line_nr++;
2565
            }
2566
            // No need to drag this
2567
            unset($lines);
2568
            $reportxml .= '</tempdoc>';
2569
            // Save original values
2570
            $this->parser_stack[] = $this->parser;
2571
            $oldgedrec            = $this->gedrec;
2572
2573
            $this->list_total   = count($this->list);
2574
            $this->list_private = 0;
2575
            foreach ($this->list as $record) {
2576
                if ($record->canShow()) {
2577
                    $this->gedrec = $record->privatizeGedcom(Auth::accessLevel($record->tree()));
2578
                    //-- start the sax parser
2579
                    $repeat_parser = xml_parser_create();
2580
                    $this->parser  = $repeat_parser;
2581
                    xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
2582
2583
                    xml_set_element_handler(
2584
                        $repeat_parser,
2585
                        function ($parser, string $name, array $attrs): void {
2586
                            $this->startElement($parser, $name, $attrs);
2587
                        },
2588
                        function ($parser, string $name): void {
2589
                            $this->endElement($parser, $name);
2590
                        }
2591
                    );
2592
2593
                    xml_set_character_data_handler(
2594
                        $repeat_parser,
2595
                        function ($parser, string $data): void {
2596
                            $this->characterData($parser, $data);
2597
                        }
2598
                    );
2599
2600
                    if (!xml_parse($repeat_parser, $reportxml, true)) {
2601
                        throw new DomainException(sprintf(
2602
                            'ListEHandler XML error: %s at line %d',
2603
                            xml_error_string(xml_get_error_code($repeat_parser)),
2604
                            xml_get_current_line_number($repeat_parser)
2605
                        ));
2606
                    }
2607
                    xml_parser_free($repeat_parser);
2608
                } else {
2609
                    $this->list_private++;
2610
                }
2611
            }
2612
            $this->list   = [];
2613
            $this->parser = array_pop($this->parser_stack);
2614
            $this->gedrec = $oldgedrec;
2615
        }
2616
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
2617
    }
2618
2619
    /**
2620
     * Handle <listTotal>
2621
     * Prints the total number of records in a list
2622
     * The total number is collected from <list> and <relatives>
2623
     *
2624
     * @return void
2625
     */
2626
    protected function listTotalStartHandler(): void
2627
    {
2628
        if ($this->list_private == 0) {
2629
            $this->current_element->addText((string) $this->list_total);
2630
        } else {
2631
            $this->current_element->addText(($this->list_total - $this->list_private) . ' / ' . $this->list_total);
2632
        }
2633
    }
2634
2635
    /**
2636
     * Handle <relatives>
2637
     *
2638
     * @param array<string> $attrs
2639
     *
2640
     * @return void
2641
     */
2642
    protected function relativesStartHandler(array $attrs): void
2643
    {
2644
        $this->process_repeats++;
2645
        if ($this->process_repeats > 1) {
2646
            return;
2647
        }
2648
2649
        $sortby = $attrs['sortby'] ?? 'NAME';
2650
2651
        $match = [];
2652
        if (preg_match("/\\$(\w+)/", $sortby, $match)) {
2653
            $sortby = $this->vars[$match[1]]['id'];
2654
            $sortby = trim($sortby);
2655
        }
2656
2657
        $maxgen = -1;
2658
        if (isset($attrs['maxgen'])) {
2659
            $maxgen = (int) $attrs['maxgen'];
2660
        }
2661
2662
        $group = $attrs['group'] ?? 'child-family';
2663
2664
        if (preg_match("/\\$(\w+)/", $group, $match)) {
2665
            $group = $this->vars[$match[1]]['id'];
2666
            $group = trim($group);
2667
        }
2668
2669
        $id = $attrs['id'] ?? '';
2670
2671
        if (preg_match("/\\$(\w+)/", $id, $match)) {
2672
            $id = $this->vars[$match[1]]['id'];
2673
            $id = trim($id);
2674
        }
2675
2676
        $this->list = [];
2677
        $person     = Registry::individualFactory()->make($id, $this->tree);
2678
        if ($person instanceof Individual) {
2679
            $this->list[$id] = $person;
2680
            $this->mfrelation[$id] = "";
2681
            $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...
2682
            switch ($group) {
2683
                case 'child-family':
2684
                    foreach ($person->childFamilies() as $family) {
2685
                        foreach ($family->spouses() as $spouse) {
2686
                            $this->list[$spouse->xref()] = $spouse;
2687
                        }
2688
2689
                        foreach ($family->children() as $child) {
2690
                            $this->list[$child->xref()] = $child;
2691
                        }
2692
                    }
2693
                    break;
2694
                case 'spouse-family':
2695
                    foreach ($person->spouseFamilies() as $family) {
2696
                        foreach ($family->spouses() as $spouse) {
2697
                            $this->list[$spouse->xref()] = $spouse;
2698
                        }
2699
2700
                        foreach ($family->children() as $child) {
2701
                            $this->list[$child->xref()] = $child;
2702
                        }
2703
                    }
2704
                    break;
2705
                case 'direct-ancestors':
2706
                    $this->addAncestors($this->list, $id, false, $maxgen);
2707
                    break;
2708
                case 'ancestors':
2709
                    $this->addAncestors($this->list, $id, true, $maxgen);
2710
                    break;
2711
                case 'descendants':
2712
                    $this->list[$id]->generation = 1;
2713
                    $this->addDescendancy($this->list, $id, false, $maxgen);
2714
                    break;
2715
                case 'all':
2716
                    $this->addAncestors($this->list, $id, true, $maxgen);
2717
                    $this->addDescendancy($this->list, $id, true, $maxgen);
2718
                    break;
2719
            }
2720
        }
2721
2722
        switch ($sortby) {
2723
            case 'NAME':
2724
                uasort($this->list, GedcomRecord::nameComparator());
2725
                break;
2726
            case 'BIRT:DATE':
2727
                uasort($this->list, Individual::birthDateComparator());
2728
                break;
2729
            case 'DEAT:DATE':
2730
                uasort($this->list, Individual::deathDateComparator());
2731
                break;
2732
            case 'generation':
2733
                $newarray = [];
2734
                reset($this->list);
2735
                $genCounter = 1;
2736
                while (count($newarray) < count($this->list)) {
2737
                    foreach ($this->list as $key => $value) {
2738
                        if ($value->generation < 0) {
2739
                            // indication of husband or wife
2740
                            $this->generation = -$value->generation;
2741
                        }
2742
                        else {
2743
                            $this->generation = $value->generation;
2744
                        }
2745
                        if ($this->generation == $genCounter) {
2746
                            $newarray[$key] = (object) ['generation' => $this->generation];
2747
                        }
2748
                    }
2749
                    $genCounter++;
2750
                }
2751
                $this->list = $newarray;
2752
                break;
2753
            default:
2754
                // unsorted
2755
                break;
2756
        }
2757
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
2758
        $this->repeat_bytes    = xml_get_current_line_number($this->parser) + 1;
2759
    }
2760
2761
    /**
2762
     * Handle </relatives>
2763
     *
2764
     * @return void
2765
     */
2766
    protected function relativesEndHandler(): void
2767
    {
2768
        $this->process_repeats--;
2769
        if ($this->process_repeats > 0) {
2770
            return;
2771
        }
2772
2773
        // Check if there is any relatives
2774
        if (count($this->list) > 0) {
2775
            $lineoffset = 0;
2776
            foreach ($this->repeats_stack as $rep) {
2777
                $lineoffset += $rep[1] - 1;
2778
            }
2779
            //-- read the xml from the file
2780
            $lines = file($this->report);
2781
            while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<Relatives') && $lineoffset + $this->repeat_bytes > 0) {
2782
                $lineoffset--;
2783
            }
2784
            $lineoffset++;
2785
            $reportxml = "<tempdoc>\n";
2786
            $line_nr   = $lineoffset + $this->repeat_bytes;
2787
            // Relatives Level counter
2788
            $count = 1;
2789
            while (0 < $count) {
2790
                if (str_contains($lines[$line_nr], '<Relatives')) {
2791
                    $count++;
2792
                } elseif (str_contains($lines[$line_nr], '</Relatives')) {
2793
                    $count--;
2794
                }
2795
                if (0 < $count) {
2796
                    $reportxml .= $lines[$line_nr];
2797
                }
2798
                $line_nr++;
2799
            }
2800
            // No need to drag this
2801
            unset($lines);
2802
            $reportxml .= "</tempdoc>\n";
2803
            // Save original values
2804
            $this->parser_stack[] = $this->parser;
2805
            $oldgedrec            = $this->gedrec;
2806
2807
            $this->list_total   = count($this->list);
2808
            $this->list_private = 0;
2809
            foreach ($this->list as $key => $value) {
2810
                if (isset($value->generation)) {
2811
                    $this->generation = $value->generation;
2812
                }
2813
                $xref = $key;
2814
                $this->vars["dupl"]["id"] = "no";
2815
                if (substr($key, 0, 2) == "D_") {
2816
                    $xref = substr($key, strrpos($key, "_") + 1);
2817
                    $this->vars["dupl"]["id"] = "yes";
2818
                }
2819
                $tmp          = Registry::gedcomRecordFactory()->make((string) $xref, $this->tree);
2820
                $this->gedrec = $tmp->privatizeGedcom(Auth::accessLevel($this->tree));
2821
2822
                $repeat_parser = xml_parser_create();
2823
                $this->parser  = $repeat_parser;
2824
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
2825
2826
                xml_set_element_handler(
2827
                    $repeat_parser,
2828
                    function ($parser, string $name, array $attrs): void {
2829
                        $this->startElement($parser, $name, $attrs);
2830
                    },
2831
                    function ($parser, string $name): void {
2832
                        $this->endElement($parser, $name);
2833
                    }
2834
                );
2835
2836
                xml_set_character_data_handler(
2837
                    $repeat_parser,
2838
                    function ($parser, string $data): void {
2839
                        $this->characterData($parser, $data);
2840
                    }
2841
                );
2842
2843
                if (!xml_parse($repeat_parser, $reportxml, true)) {
2844
                    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)));
2845
                }
2846
                xml_parser_free($repeat_parser);
2847
            }
2848
            // Clean up the list array
2849
            $this->list   = [];
2850
            $this->parser = array_pop($this->parser_stack);
2851
            $this->gedrec = $oldgedrec;
2852
        }
2853
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
2854
    }
2855
2856
    /**
2857
     * Handle <generation />
2858
     * Prints the number of generations
2859
     *
2860
     * @return void
2861
     */
2862
    protected function generationStartHandler(): void
2863
    {
2864
        $this->current_element->addText((string) $this->generation);
2865
    }
2866
2867
    /**
2868
     * Handle <newPage />
2869
     * Has to be placed in an element (header, body or footer)
2870
     *
2871
     * @return void
2872
     */
2873
    protected function newPageStartHandler(): void
2874
    {
2875
        $temp = 'addpage';
2876
        $this->wt_report->addElement($temp);
2877
    }
2878
2879
    /**
2880
     * Handle </title>
2881
     *
2882
     * @return void
2883
     */
2884
    protected function titleEndHandler(): void
2885
    {
2886
        $this->report_root->addTitle($this->text);
2887
    }
2888
2889
    /**
2890
     * Handle </description>
2891
     *
2892
     * @return void
2893
     */
2894
    protected function descriptionEndHandler(): void
2895
    {
2896
        $this->report_root->addDescription($this->text);
2897
    }
2898
2899
    /**
2900
     * Create a list of all descendants.
2901
     *
2902
     * @param array<Individual> $list
2903
     * @param string            $pid
2904
     * @param bool              $parents
2905
     * @param int               $generations
2906
     *
2907
     * @return void
2908
     */
2909
    private function addDescendancy(&$list, $pid, $parents = false, $generations = -1): void
2910
    {
2911
        $person = Registry::individualFactory()->make($pid, $this->tree);
2912
        if ($person === null) {
2913
            return;
2914
        }
2915
2916
        static $focusperson = true;
2917
        static $dupl = 1;
2918
        $sx = $person->sex();
2919
        $rl = "x"; // unknown
2920
        if ($sx == "M") {
2921
            $rl = "s";
2922
        } // son
2923
        if ($sx == "F") {
2924
            $rl = "d";
0 ignored issues
show
Unused Code introduced by
The assignment to $rl is dead and can be removed.
Loading history...
2925
        } // daughter
2926
        if ($focusperson) {
2927
            $this->mfrelation[$pid] = "";
2928
        }
2929
        $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...
2930
2931
        $newpid = $pid;
2932
        if (!isset($list[$pid])) {
2933
            $list[$pid] = $person;
2934
        } elseif (!$focusperson) {
2935
            $newpid = "D_" . $dupl . "_" . $pid;
2936
            $list[$newpid] = $person;
2937
        }
2938
        if (!isset($list[$newpid]->generation)) {
2939
            $list[$newpid]->generation = 0;
2940
        }
2941
        $focusperson = false;
2942
        foreach ($person->spouseFamilies() as $family) {
2943
            if ($parents) {
2944
                $husband = $family->husband();
2945
                $wife    = $family->wife();
2946
                if ($husband) {
2947
                    $list[$husband->xref()] = $husband;
2948
                    if (isset($list[$pid]->generation)) {
2949
                        $list[$husband->xref()]->generation = $list[$pid]->generation - 1;
2950
                    } else {
2951
                        $list[$husband->xref()]->generation = 1;
2952
                    }
2953
                }
2954
                if ($wife) {
2955
                    $list[$wife->xref()] = $wife;
2956
                    if (isset($list[$pid]->generation)) {
2957
                        $list[$wife->xref()]->generation = $list[$pid]->generation - 1;
2958
                    } else {
2959
                        $list[$wife->xref()]->generation = 1;
2960
                    }
2961
                }
2962
            }
2963
            $husband = $family->husband();
2964
            $wife = $family->wife();
2965
2966
            if ($husband && $wife) {
2967
                if ($husband->xref() == $person->xref()) {
2968
                    $this->mfrelation[$wife->xref()] = $this->mfrelation[$person->xref()] . "x";
2969
                    if ($wife->canShow()) {
2970
                        $list[$wife->xref()] = $wife;
2971
                    }
2972
                    if (!isset($wife->generation)) {
2973
                        $wife->generation = $person->generation;
2974
                    }
2975
                    $nam = $wife->getAllNames()[0]['fullNN'];
2976
                } else {
2977
                    $this->mfrelation[$husband->xref()] = $this->mfrelation[$person->xref()] . "x";
2978
                    if ($husband->canShow()) {
2979
                        $list[$husband->xref()] = $husband;
2980
                    }
2981
                    if (!isset($husband->generation)) {
2982
                        $husband->generation = $person->generation;
2983
                    }
2984
                    $nam = $husband->getAllNames()[0]['fullNN'];
2985
                }
2986
            }
2987
2988
            $children = $family->children();
2989
            foreach ($children as $child) {
2990
                if ($child) {
2991
                    $sx = $child->sex();
2992
                    $rl = "x"; // unknown
2993
                    if ($sx == "M") {
2994
                        $rl = "s";
2995
                    } // son
2996
                    if ($sx == "F") {
2997
                        $rl = "d";
2998
                    } // daughter
2999
                    $rl = $this->mfrelation[$person->xref()] . $rl;
3000
                    $this->mfrelation[$child->xref()] = $rl;
3001
                    if (isset($list[$pid]->generation)) {
3002
                        $child->generation = $list[$pid]->generation + 1;
3003
                    } else {
3004
                        $child->generation = 2;
3005
                    }
3006
                }
3007
            }
3008
            if ($generations == -1 || $list[$pid]->generation < $generations) {
3009
                foreach ($children as $child) {
3010
                    if ($child->canShow()) {
3011
                        $this->addDescendancy($list, $child->xref(), $parents, $generations);
3012
                    } // recurse on the childs family
3013
                }
3014
            }
3015
        }
3016
        $focusperson = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $focusperson is dead and can be removed.
Loading history...
3017
    }
3018
3019
    /**
3020
     * Create a list of all ancestors.
3021
     *
3022
     * @param array<Individual> $list
3023
     * @param string            $pid
3024
     * @param bool              $children
3025
     * @param int               $generations
3026
     *
3027
     * @return void
3028
     */
3029
    private function addAncestors(array &$list, string $pid, bool $children = false, int $generations = -1): void
3030
    {
3031
        $genlist                = [$pid];
3032
        $list[$pid]->generation = 1;
3033
        while (count($genlist) > 0) {
3034
            $id = array_shift($genlist);
3035
            if (str_starts_with($id, 'empty')) {
3036
                continue; // id can be something like “empty7”
3037
            }
3038
            if (!isset($this->mfrelation[$id])) {
3039
                $this->mfrelation[$id] = "";
3040
            }
3041
            $person = Registry::individualFactory()->make($id, $this->tree);
3042
            foreach ($person->childFamilies() as $family) {
3043
                $husband = $family->husband();
3044
                $wife    = $family->wife();
3045
                if ($husband) {
3046
                    $list[$husband->xref()]             = $husband;
3047
                    $list[$husband->xref()]->generation = $list[$id]->generation + 1;
3048
                    $this->mfrelation[$husband->xref()] = $this->mfrelation[$id] . "f";
3049
                }
3050
                if ($wife) {
3051
                    $list[$wife->xref()]             = $wife;
3052
                    $list[$wife->xref()]->generation = $list[$id]->generation + 1;
3053
                    $this->mfrelation[$wife->xref()] = $this->mfrelation[$id] . "m";
3054
                }
3055
                if ($generations == -1 || $list[$id]->generation + 1 < $generations) {
3056
                    if ($husband) {
3057
                        $genlist[] = $husband->xref();
3058
                    }
3059
                    if ($wife) {
3060
                        $genlist[] = $wife->xref();
3061
                    }
3062
                }
3063
                if ($children) {
3064
                    foreach ($family->children() as $child) {
3065
                        $list[$child->xref()] = $child;
3066
                        $child->generation = $list[$id]->generation ?? 1;
3067
                        if ($child->xref() != $person->xref()) {
3068
                            $this->mfrelation[$child->xref()] = $this->mfrelation[$id] . "x";
3069
                        }
3070
                    }
3071
                }
3072
            }
3073
        }
3074
    }
3075
3076
    /**
3077
     * get gedcom tag value
3078
     *
3079
     * @param string $tag    The tag to find, use : to delineate subtags
3080
     * @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
3081
     * @param string $gedrec The gedcom record to get the value from
3082
     *
3083
     * @return string the value of a gedcom tag from the given gedcom record
3084
     */
3085
    private function getGedcomValue(string $tag, int $level, string $gedrec): string
3086
    {
3087
        if ($gedrec === '') {
3088
            return '';
3089
        }
3090
        $tags      = explode(':', $tag);
3091
        $origlevel = $level;
3092
        if ($level === 0) {
3093
            $level = $gedrec[0] + 1;
3094
        }
3095
3096
        $subrec = $gedrec;
3097
        $t = 'XXXX';
3098
        foreach ($tags as $t) {
3099
            $lastsubrec = $subrec;
3100
            $subrec     = self::getSubRecord($level, "$level $t", $subrec);
3101
            if (empty($subrec) && $origlevel == 0) {
3102
                $level--;
3103
                $subrec = self::getSubRecord($level, "$level $t", $lastsubrec);
3104
            }
3105
            if (empty($subrec)) {
3106
                if ($t === 'TITL') {
3107
                    $subrec = self::getSubRecord($level, "$level ABBR", $lastsubrec);
3108
                    if (!empty($subrec)) {
3109
                        $t = 'ABBR';
3110
                    }
3111
                }
3112
                if ($subrec === '') {
3113
                    if ($level > 0) {
3114
                        $level--;
3115
                    }
3116
                    $subrec = self::getSubRecord($level, "@ $t", $gedrec);
3117
                    if ($subrec === '') {
3118
                        return '';
3119
                    }
3120
                }
3121
            }
3122
            $level++;
3123
        }
3124
        $level--;
3125
        $ct = preg_match("/$level $t(.*)/", $subrec, $match);
3126
        if ($ct === 0) {
3127
            $ct = preg_match("/$level @.+@ (.+)/", $subrec, $match);
3128
        }
3129
        if ($ct === 0) {
3130
            $ct = preg_match("/@ $t (.+)/", $subrec, $match);
3131
        }
3132
        if ($ct > 0) {
3133
            $value = trim($match[1]);
3134
            if ($t === 'NOTE' && preg_match('/^@(.+)@$/', $value, $match)) {
3135
                $note = Registry::noteFactory()->make($match[1], $this->tree);
3136
                if ($note instanceof Note) {
3137
                    $value = $note->getNote();
3138
                } else {
3139
                    //-- set the value to the id without the @
3140
                    $value = $match[1];
3141
                }
3142
            }
3143
            if ($level !== 0 || $t !== 'NOTE') {
3144
                $value .= self::getCont($level + 1, $subrec);
3145
            }
3146
3147
            if ($tag === 'NAME' || $tag === '_MARNM' || $tag === '_AKA') {
3148
                return strtr($value, ['/' => '']);
3149
            }
3150
3151
            if ($tag === 'NAME' || $tag === '_MARNM' || $tag === '_AKA') {
3152
                return strtr($value, ['/' => '']);
3153
            }
3154
3155
            return $value;
3156
        }
3157
3158
        return '';
3159
    }
3160
3161
    /**
3162
     * Replace variable identifiers with their values.
3163
     *
3164
     * @param string $expression An expression such as "$foo == 123"
3165
     * @param bool   $quote      Whether to add quotation marks
3166
     *
3167
     * @return string
3168
     */
3169
    private function substituteVars($expression, $quote): string
3170
    {
3171
        return preg_replace_callback(
3172
            '/\$(\w+)/',
3173
            function (array $matches) use ($quote): string {
3174
                if (isset($this->vars[$matches[1]]['id'])) {
3175
                    if ($quote) {
3176
                        return "'" . addcslashes($this->vars[$matches[1]]['id'], "'") . "'";
3177
                    }
3178
3179
                    return $this->vars[$matches[1]]['id'];
3180
                }
3181
3182
                Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1]));
3183
3184
                return '$' . $matches[1];
3185
            },
3186
            $expression
3187
        );
3188
    }
3189
}
3190