ReportParserGenerate::getSubRecord()   B
last analyzed

Complexity

Conditions 7
Paths 11

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 20
nc 11
nop 4
dl 0
loc 30
rs 8.6666
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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 League\Flysystem\FilesystemOperator;
44
use LogicException;
45
use Symfony\Component\Cache\Adapter\NullAdapter;
46
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
47
use XMLParser;
48
49
use function addcslashes;
50
use function addslashes;
51
use function array_pop;
52
use function array_shift;
53
use function assert;
54
use function count;
55
use function end;
56
use function explode;
57
use function file;
58
use function file_exists;
59
use function getimagesize;
60
use function imagecreatefromstring;
61
use function imagesx;
62
use function imagesy;
63
use function in_array;
64
use function ltrim;
65
use function method_exists;
66
use function preg_match;
67
use function preg_match_all;
68
use function preg_replace;
69
use function preg_replace_callback;
70
use function preg_split;
71
use function reset;
72
use function round;
73
use function sprintf;
74
use function str_contains;
75
use function str_ends_with;
76
use function str_replace;
77
use function str_starts_with;
78
use function strip_tags;
79
use function strlen;
80
use function strpos;
81
use function strtoupper;
82
use function substr;
83
use function 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
    /** @var XMLParser (resource before PHP 8.0) Nested repeating data */
135
    private $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 Tree $tree;
186
187
    /**
188
     * Create a parser for a report
189
     *
190
     * @param string               $report The XML filename
191
     * @param AbstractRenderer     $report_root
192
     * @param array<array<string>> $vars
193
     * @param Tree                 $tree
194
     */
195
    public function __construct(string $report, AbstractRenderer $report_root, array $vars, Tree $tree)
196
    {
197
        $this->report          = $report;
198
        $this->report_root     = $report_root;
199
        $this->wt_report       = $report_root;
200
        $this->current_element = new ReportBaseElement();
201
        $this->vars            = $vars;
202
        $this->tree            = $tree;
203
204
        parent::__construct($report);
205
    }
206
207
    /**
208
     * get a gedcom subrecord
209
     *
210
     * searches a gedcom record and returns a subrecord of it. A subrecord is defined starting at a
211
     * line with level N and all subsequent lines greater than N until the next N level is reached.
212
     * For example, the following is a BIRT subrecord:
213
     * <code>1 BIRT
214
     * 2 DATE 1 JAN 1900
215
     * 2 PLAC Phoenix, Maricopa, Arizona</code>
216
     * The following example is the DATE subrecord of the above BIRT subrecord:
217
     * <code>2 DATE 1 JAN 1900</code>
218
     *
219
     * @param int    $level   the N level of the subrecord to get
220
     * @param string $tag     a gedcom tag or string to search for in the record (ie 1 BIRT or 2 DATE)
221
     * @param string $gedrec  the parent gedcom record to search in
222
     * @param int    $num     this allows you to specify which matching <var>$tag</var> to get. Oftentimes a
223
     *                        gedcom record will have more that 1 of the same type of subrecord. An individual may have
224
     *                        multiple events for example. Passing $num=1 would get the first 1. Passing $num=2 would get the
225
     *                        second one, etc.
226
     *
227
     * @return string the subrecord that was found or an empty string "" if not found.
228
     */
229
    public static function getSubRecord(int $level, string $tag, string $gedrec, int $num = 1): string
230
    {
231
        if ($gedrec === '') {
232
            return '';
233
        }
234
        // -- adding \n before and after gedrec
235
        $gedrec       = "\n" . $gedrec . "\n";
236
        $tag          = trim($tag);
237
        $searchTarget = "~[\n]" . $tag . "[\s]~";
238
        $ct           = preg_match_all($searchTarget, $gedrec, $match, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
239
        if ($ct === 0) {
240
            return '';
241
        }
242
        if ($ct < $num) {
243
            return '';
244
        }
245
        $pos1 = (int) $match[$num - 1][0][1];
246
        $pos2 = strpos($gedrec, "\n$level", $pos1 + 1);
247
        if (!$pos2) {
248
            $pos2 = strpos($gedrec, "\n1", $pos1 + 1);
249
        }
250
        if (!$pos2) {
251
            $pos2 = strpos($gedrec, "\nWT_", $pos1 + 1); // WT_SPOUSE, WT_FAMILY_ID ...
252
        }
253
        if (!$pos2) {
254
            return ltrim(substr($gedrec, $pos1));
255
        }
256
        $subrec = substr($gedrec, $pos1, $pos2 - $pos1);
257
258
        return ltrim($subrec);
259
    }
260
261
    /**
262
     * get CONT lines
263
     *
264
     * get the N+1 CONT or CONC lines of a gedcom subrecord
265
     *
266
     * @param int    $nlevel the level of the CONT lines to get
267
     * @param string $nrec   the gedcom subrecord to search in
268
     *
269
     * @return string a string with all CONT lines merged
270
     */
271
    public static function getCont(int $nlevel, string $nrec): string
272
    {
273
        $text = '';
274
275
        $subrecords = explode("\n", $nrec);
276
        foreach ($subrecords as $thisSubrecord) {
277
            if (substr($thisSubrecord, 0, 2) !== $nlevel . ' ') {
278
                continue;
279
            }
280
            $subrecordType = substr($thisSubrecord, 2, 4);
281
            if ($subrecordType === 'CONT') {
282
                $text .= "\n" . substr($thisSubrecord, 7);
283
            }
284
        }
285
286
        return $text;
287
    }
288
289
    /**
290
     * XML start element handler
291
     * This function is called whenever a starting element is reached
292
     * The element handler will be called if found, otherwise it must be HTML
293
     *
294
     * @param resource      $parser the resource handler for the XML parser
295
     * @param string        $name   the name of the XML element parsed
296
     * @param array<string> $attrs  an array of key value pairs for the attributes
297
     *
298
     * @return void
299
     */
300
    protected function startElement($parser, string $name, array $attrs): void
301
    {
302
        $newattrs = [];
303
304
        foreach ($attrs as $key => $value) {
305
            if (preg_match("/^\\$(\w+)$/", $value, $match)) {
306
                if (isset($this->vars[$match[1]]['id']) && !isset($this->vars[$match[1]]['gedcom'])) {
307
                    $value = $this->vars[$match[1]]['id'];
308
                }
309
            }
310
            $newattrs[$key] = $value;
311
        }
312
        $attrs = $newattrs;
313
        if ($this->process_footnote && ($this->process_ifs === 0 || $name === 'if') && ($this->process_gedcoms === 0 || $name === 'Gedcom') && ($this->process_repeats === 0 || $name === 'Facts' || $name === 'RepeatTag')) {
314
            $method = $name . 'StartHandler';
315
316
            if (method_exists($this, $method)) {
317
                $this->{$method}($attrs);
318
            }
319
        }
320
    }
321
322
    /**
323
     * XML end element handler
324
     * This function is called whenever an ending element is reached
325
     * The element handler will be called if found, otherwise it must be HTML
326
     *
327
     * @param resource $parser the resource handler for the XML parser
328
     * @param string   $name   the name of the XML element parsed
329
     *
330
     * @return void
331
     */
332
    protected function endElement($parser, string $name): void
333
    {
334
        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')) {
335
            $method = $name . 'EndHandler';
336
337
            if (method_exists($this, $method)) {
338
                $this->{$method}();
339
            }
340
        }
341
    }
342
343
    /**
344
     * XML character data handler
345
     *
346
     * @param resource $parser the resource handler for the XML parser
347
     * @param string   $data   the name of the XML element parsed
348
     *
349
     * @return void
350
     */
351
    protected function characterData($parser, string $data): void
352
    {
353
        if ($this->print_data && $this->process_gedcoms === 0 && $this->process_ifs === 0 && $this->process_repeats === 0) {
354
            $this->current_element->addText($data);
355
        }
356
    }
357
358
    /**
359
     * Handle <style>
360
     *
361
     * @param array<string> $attrs
362
     *
363
     * @return void
364
     */
365
    protected function styleStartHandler(array $attrs): void
366
    {
367
        if (empty($attrs['name'])) {
368
            throw new DomainException('REPORT ERROR Style: The "name" of the style is missing or not set in the XML file.');
369
        }
370
371
        $style = [
372
            'name'  => $attrs['name'],
373
            'font'  => $attrs['font'] ?? $this->wt_report->default_font,
374
            'size'  => (float) ($attrs['size'] ?? $this->wt_report->default_font_size),
375
            'style' => $attrs['style'] ?? '',
376
        ];
377
378
        $this->wt_report->addStyle($style);
379
    }
380
381
    /**
382
     * Handle <doc>
383
     * Sets up the basics of the document proparties
384
     *
385
     * @param array<string> $attrs
386
     *
387
     * @return void
388
     */
389
    protected function docStartHandler(array $attrs): void
390
    {
391
        $this->parser = $this->xml_parser;
392
393
        // Custom page width
394
        if (!empty($attrs['customwidth'])) {
395
            $this->wt_report->page_width = (float) $attrs['customwidth'];
396
        }
397
        // Custom Page height
398
        if (!empty($attrs['customheight'])) {
399
            $this->wt_report->page_height = (float) $attrs['customheight'];
400
        }
401
402
        // Left Margin
403
        if (isset($attrs['leftmargin'])) {
404
            if ($attrs['leftmargin'] === '0') {
405
                $this->wt_report->left_margin = 0;
406
            } elseif (!empty($attrs['leftmargin'])) {
407
                $this->wt_report->left_margin = (float) $attrs['leftmargin'];
408
            }
409
        }
410
        // Right Margin
411
        if (isset($attrs['rightmargin'])) {
412
            if ($attrs['rightmargin'] === '0') {
413
                $this->wt_report->right_margin = 0;
414
            } elseif (!empty($attrs['rightmargin'])) {
415
                $this->wt_report->right_margin = (float) $attrs['rightmargin'];
416
            }
417
        }
418
        // Top Margin
419
        if (isset($attrs['topmargin'])) {
420
            if ($attrs['topmargin'] === '0') {
421
                $this->wt_report->top_margin = 0;
422
            } elseif (!empty($attrs['topmargin'])) {
423
                $this->wt_report->top_margin = (float) $attrs['topmargin'];
424
            }
425
        }
426
        // Bottom Margin
427
        if (isset($attrs['bottommargin'])) {
428
            if ($attrs['bottommargin'] === '0') {
429
                $this->wt_report->bottom_margin = 0;
430
            } elseif (!empty($attrs['bottommargin'])) {
431
                $this->wt_report->bottom_margin = (float) $attrs['bottommargin'];
432
            }
433
        }
434
        // Header Margin
435
        if (isset($attrs['headermargin'])) {
436
            if ($attrs['headermargin'] === '0') {
437
                $this->wt_report->header_margin = 0;
438
            } elseif (!empty($attrs['headermargin'])) {
439
                $this->wt_report->header_margin = (float) $attrs['headermargin'];
440
            }
441
        }
442
        // Footer Margin
443
        if (isset($attrs['footermargin'])) {
444
            if ($attrs['footermargin'] === '0') {
445
                $this->wt_report->footer_margin = 0;
446
            } elseif (!empty($attrs['footermargin'])) {
447
                $this->wt_report->footer_margin = (float) $attrs['footermargin'];
448
            }
449
        }
450
451
        // Page Orientation
452
        if (!empty($attrs['orientation'])) {
453
            if ($attrs['orientation'] === 'landscape') {
454
                $this->wt_report->orientation = 'landscape';
455
            } elseif ($attrs['orientation'] === 'portrait') {
456
                $this->wt_report->orientation = 'portrait';
457
            }
458
        }
459
        // Page Size
460
        if (!empty($attrs['pageSize'])) {
461
            $this->wt_report->page_format = strtoupper($attrs['pageSize']);
462
        }
463
464
        // Show Generated By...
465
        if (isset($attrs['showGeneratedBy'])) {
466
            if ($attrs['showGeneratedBy'] === '0') {
467
                $this->wt_report->show_generated_by = false;
468
            } elseif ($attrs['showGeneratedBy'] === '1') {
469
                $this->wt_report->show_generated_by = true;
470
            }
471
        }
472
473
        $this->wt_report->setup();
474
    }
475
476
    /**
477
     * Handle </doc>
478
     *
479
     * @return void
480
     */
481
    protected function docEndHandler(): void
482
    {
483
        $this->wt_report->run();
484
    }
485
486
    /**
487
     * Handle <header>
488
     *
489
     * @return void
490
     */
491
    protected function headerStartHandler(): void
492
    {
493
        // Clear the Header before any new elements are added
494
        $this->wt_report->clearHeader();
495
        $this->wt_report->setProcessing('H');
496
    }
497
498
    /**
499
     * Handle <body>
500
     *
501
     * @return void
502
     */
503
    protected function bodyStartHandler(): void
504
    {
505
        $this->wt_report->setProcessing('B');
506
    }
507
508
    /**
509
     * Handle <footer>
510
     *
511
     * @return void
512
     */
513
    protected function footerStartHandler(): void
514
    {
515
        $this->wt_report->setProcessing('F');
516
    }
517
518
    /**
519
     * Handle <cell>
520
     *
521
     * @param array<string,string> $attrs
522
     *
523
     * @return void
524
     */
525
    protected function cellStartHandler(array $attrs): void
526
    {
527
        // string The text alignment of the text in this box.
528
        $align = $attrs['align'] ?? '';
529
        // RTL supported left/right alignment
530
        if ($align === 'rightrtl') {
531
            if ($this->wt_report->rtl) {
532
                $align = 'left';
533
            } else {
534
                $align = 'right';
535
            }
536
        } elseif ($align === 'leftrtl') {
537
            if ($this->wt_report->rtl) {
538
                $align = 'right';
539
            } else {
540
                $align = 'left';
541
            }
542
        }
543
544
        // The color to fill the background of this cell
545
        $bgcolor = $attrs['bgcolor'] ?? '';
546
547
        // Whether the background should be painted
548
        $fill = (bool) ($attrs['fill'] ?? '0');
549
550
        // If true reset the last cell height
551
        $reseth = (bool) ($attrs['reseth'] ?? '1');
552
553
        // Whether a border should be printed around this box
554
        $border = $attrs['border'] ?? '';
555
556
        // string Border color in HTML code
557
        $bocolor = $attrs['bocolor'] ?? '';
558
559
        // Cell height (expressed in points) The starting height of this cell. If the text wraps the height will automatically be adjusted.
560
        $height = (int) ($attrs['height'] ?? '0');
561
562
        // int Cell width (expressed in points) Setting the width to 0 will make it the width from the current location to the right margin.
563
        $width = (int) ($attrs['width'] ?? '0');
564
565
        // Stretch character mode
566
        $stretch = (int) ($attrs['stretch'] ?? '0');
567
568
        // mixed Position the left corner of this box on the page. The default is the current position.
569
        $left = ReportBaseElement::CURRENT_POSITION;
570
        if (isset($attrs['left'])) {
571
            if ($attrs['left'] === '.') {
572
                $left = ReportBaseElement::CURRENT_POSITION;
573
            } elseif (!empty($attrs['left'])) {
574
                $left = (float) $attrs['left'];
575
            } elseif ($attrs['left'] === '0') {
576
                $left = 0.0;
577
            }
578
        }
579
        // mixed Position the top corner of this box on the page. the default is the current position
580
        $top = ReportBaseElement::CURRENT_POSITION;
581
        if (isset($attrs['top'])) {
582
            if ($attrs['top'] === '.') {
583
                $top = ReportBaseElement::CURRENT_POSITION;
584
            } elseif (!empty($attrs['top'])) {
585
                $top = (float) $attrs['top'];
586
            } elseif ($attrs['top'] === '0') {
587
                $top = 0.0;
588
            }
589
        }
590
591
        // The name of the Style that should be used to render the text.
592
        $style = $attrs['style'] ?? '';
593
594
        // string Text color in html code
595
        $tcolor = $attrs['tcolor'] ?? '';
596
597
        // int Indicates where the current position should go after the call.
598
        $ln = 0;
599
        if (isset($attrs['newline'])) {
600
            if (!empty($attrs['newline'])) {
601
                $ln = (int) $attrs['newline'];
602
            } elseif ($attrs['newline'] === '0') {
603
                $ln = 0;
604
            }
605
        }
606
607
        if ($align === 'left') {
608
            $align = 'L';
609
        } elseif ($align === 'right') {
610
            $align = 'R';
611
        } elseif ($align === 'center') {
612
            $align = 'C';
613
        } elseif ($align === 'justify') {
614
            $align = 'J';
615
        }
616
617
        $this->print_data_stack[] = $this->print_data;
618
        $this->print_data         = true;
619
620
        $this->current_element = $this->report_root->createCell(
621
            $width,
622
            $height,
623
            $border,
624
            $align,
625
            $bgcolor,
626
            $style,
627
            $ln,
628
            $top,
629
            $left,
630
            $fill,
631
            $stretch,
632
            $bocolor,
633
            $tcolor,
634
            $reseth
635
        );
636
    }
637
638
    /**
639
     * Handle </cell>
640
     *
641
     * @return void
642
     */
643
    protected function cellEndHandler(): void
644
    {
645
        $this->print_data = array_pop($this->print_data_stack);
646
        $this->wt_report->addElement($this->current_element);
647
    }
648
649
    /**
650
     * Handle <now />
651
     *
652
     * @return void
653
     */
654
    protected function nowStartHandler(): void
655
    {
656
        $this->current_element->addText(Registry::timestampFactory()->now()->isoFormat('LLLL'));
657
    }
658
659
    /**
660
     * Handle <pageNum />
661
     *
662
     * @return void
663
     */
664
    protected function pageNumStartHandler(): void
665
    {
666
        $this->current_element->addText('#PAGENUM#');
667
    }
668
669
    /**
670
     * Handle <totalPages />
671
     *
672
     * @return void
673
     */
674
    protected function totalPagesStartHandler(): void
675
    {
676
        $this->current_element->addText('{{:ptp:}}');
677
    }
678
679
    /**
680
     * Called at the start of an element.
681
     *
682
     * @param array<string> $attrs an array of key value pairs for the attributes
683
     *
684
     * @return void
685
     */
686
    protected function gedcomStartHandler(array $attrs): void
687
    {
688
        if ($this->process_gedcoms > 0) {
689
            $this->process_gedcoms++;
690
691
            return;
692
        }
693
694
        $tag       = $attrs['id'];
695
        $tag       = str_replace('@fact', $this->fact, $tag);
696
        $tags      = explode(':', $tag);
697
        $newgedrec = '';
698
        if (count($tags) < 2) {
699
            $tmp       = Registry::gedcomRecordFactory()->make($attrs['id'], $this->tree);
700
            $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($this->tree)) : '';
701
        }
702
        if (empty($newgedrec)) {
703
            $tgedrec   = $this->gedrec;
704
            $newgedrec = '';
705
            foreach ($tags as $tag) {
706
                if (preg_match('/\$(.+)/', $tag, $match)) {
707
                    if (isset($this->vars[$match[1]]['gedcom'])) {
708
                        $newgedrec = $this->vars[$match[1]]['gedcom'];
709
                    } else {
710
                        $tmp       = Registry::gedcomRecordFactory()->make($match[1], $this->tree);
711
                        $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($this->tree)) : '';
712
                    }
713
                } elseif (preg_match('/@(.+)/', $tag, $match)) {
714
                    $gmatch = [];
715
                    if (preg_match("/\d $match[1] @([^@]+)@/", $tgedrec, $gmatch)) {
716
                        $tmp       = Registry::gedcomRecordFactory()->make($gmatch[1], $this->tree);
717
                        $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($this->tree)) : '';
718
                        $tgedrec   = $newgedrec;
719
                    } else {
720
                        $newgedrec = '';
721
                        break;
722
                    }
723
                } else {
724
                    $level     = 1 + (int) explode(' ', trim($tgedrec))[0];
725
                    $newgedrec = self::getSubRecord($level, "$level $tag", $tgedrec);
726
                    $tgedrec   = $newgedrec;
727
                }
728
            }
729
        }
730
        if (!empty($newgedrec)) {
731
            $this->gedrec_stack[] = [$this->gedrec, $this->fact, $this->desc];
732
            $this->gedrec         = $newgedrec;
733
            if (preg_match("/(\d+) (_?[A-Z0-9]+) (.*)/", $this->gedrec, $match)) {
734
                $this->fact = $match[2];
735
                $this->desc = trim($match[3]);
736
            }
737
        } else {
738
            $this->process_gedcoms++;
739
        }
740
    }
741
742
    /**
743
     * Called at the end of an element.
744
     *
745
     * @return void
746
     */
747
    protected function gedcomEndHandler(): void
748
    {
749
        if ($this->process_gedcoms > 0) {
750
            $this->process_gedcoms--;
751
        } else {
752
            [$this->gedrec, $this->fact, $this->desc] = array_pop($this->gedrec_stack);
753
        }
754
    }
755
756
    /**
757
     * Handle <textBox>
758
     *
759
     * @param array<string> $attrs
760
     *
761
     * @return void
762
     */
763
    protected function textBoxStartHandler(array $attrs): void
764
    {
765
        // string Background color code
766
        $bgcolor = '';
767
        if (!empty($attrs['bgcolor'])) {
768
            $bgcolor = $attrs['bgcolor'];
769
        }
770
771
        // boolean Wether or not fill the background color
772
        $fill = true;
773
        if (isset($attrs['fill'])) {
774
            if ($attrs['fill'] === '0') {
775
                $fill = false;
776
            } elseif ($attrs['fill'] === '1') {
777
                $fill = true;
778
            }
779
        }
780
781
        // var boolean Whether or not a border should be printed around this box. 0 = no border, 1 = border. Default is 0
782
        $border = false;
783
        if (isset($attrs['border'])) {
784
            if ($attrs['border'] === '1') {
785
                $border = true;
786
            } elseif ($attrs['border'] === '0') {
787
                $border = false;
788
            }
789
        }
790
791
        // int The starting height of this cell. If the text wraps the height will automatically be adjusted
792
        $height = 0;
793
        if (!empty($attrs['height'])) {
794
            $height = (int) $attrs['height'];
795
        }
796
        // int Setting the width to 0 will make it the width from the current location to the margin
797
        $width = 0;
798
        if (!empty($attrs['width'])) {
799
            $width = (int) $attrs['width'];
800
        }
801
802
        // mixed Position the left corner of this box on the page. The default is the current position.
803
        $left = ReportBaseElement::CURRENT_POSITION;
804
        if (isset($attrs['left'])) {
805
            if ($attrs['left'] === '.') {
806
                $left = ReportBaseElement::CURRENT_POSITION;
807
            } elseif (!empty($attrs['left'])) {
808
                $left = (int) $attrs['left'];
809
            } elseif ($attrs['left'] === '0') {
810
                $left = 0;
811
            }
812
        }
813
        // mixed Position the top corner of this box on the page. the default is the current position
814
        $top = ReportBaseElement::CURRENT_POSITION;
815
        if (isset($attrs['top'])) {
816
            if ($attrs['top'] === '.') {
817
                $top = ReportBaseElement::CURRENT_POSITION;
818
            } elseif (!empty($attrs['top'])) {
819
                $top = (int) $attrs['top'];
820
            } elseif ($attrs['top'] === '0') {
821
                $top = 0;
822
            }
823
        }
824
        // 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
825
        $newline = false;
826
        if (isset($attrs['newline'])) {
827
            if ($attrs['newline'] === '1') {
828
                $newline = true;
829
            } elseif ($attrs['newline'] === '0') {
830
                $newline = false;
831
            }
832
        }
833
        // boolean
834
        $pagecheck = true;
835
        if (isset($attrs['pagecheck'])) {
836
            if ($attrs['pagecheck'] === '0') {
837
                $pagecheck = false;
838
            } elseif ($attrs['pagecheck'] === '1') {
839
                $pagecheck = true;
840
            }
841
        }
842
        // boolean Cell padding
843
        $padding = true;
844
        if (isset($attrs['padding'])) {
845
            if ($attrs['padding'] === '0') {
846
                $padding = false;
847
            } elseif ($attrs['padding'] === '1') {
848
                $padding = true;
849
            }
850
        }
851
        // boolean Reset this box Height
852
        $reseth = false;
853
        if (isset($attrs['reseth'])) {
854
            if ($attrs['reseth'] === '1') {
855
                $reseth = true;
856
            } elseif ($attrs['reseth'] === '0') {
857
                $reseth = false;
858
            }
859
        }
860
861
        // string Style of rendering
862
        $style = '';
863
864
        $this->print_data_stack[] = $this->print_data;
865
        $this->print_data         = false;
866
867
        $this->wt_report_stack[] = $this->wt_report;
868
        $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...
869
            $width,
870
            $height,
871
            $border,
872
            $bgcolor,
873
            $newline,
874
            $left,
875
            $top,
876
            $pagecheck,
877
            $style,
878
            $fill,
879
            $padding,
880
            $reseth
881
        );
882
    }
883
884
    /**
885
     * Handle <textBox>
886
     *
887
     * @return void
888
     */
889
    protected function textBoxEndHandler(): void
890
    {
891
        $this->print_data      = array_pop($this->print_data_stack);
892
        $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...
893
894
        // The TextBox handler is mis-using the wt_report attribute to store an element.
895
        // Until this can be re-designed, we need this assertion to help static analysis tools.
896
        assert($this->current_element instanceof ReportBaseElement, new LogicException());
897
898
        $this->wt_report = array_pop($this->wt_report_stack);
899
        $this->wt_report->addElement($this->current_element);
900
    }
901
902
    /**
903
     * XLM <Text>.
904
     *
905
     * @param array<string> $attrs an array of key value pairs for the attributes
906
     *
907
     * @return void
908
     */
909
    protected function textStartHandler(array $attrs): void
910
    {
911
        $this->print_data_stack[] = $this->print_data;
912
        $this->print_data         = true;
913
914
        // string The name of the Style that should be used to render the text.
915
        $style = '';
916
        if (!empty($attrs['style'])) {
917
            $style = $attrs['style'];
918
        }
919
920
        // string  The color of the text - Keep the black color as default
921
        $color = '';
922
        if (!empty($attrs['color'])) {
923
            $color = $attrs['color'];
924
        }
925
926
        $this->current_element = $this->report_root->createText($style, $color);
927
    }
928
929
    /**
930
     * Handle </text>
931
     *
932
     * @return void
933
     */
934
    protected function textEndHandler(): void
935
    {
936
        $this->print_data = array_pop($this->print_data_stack);
937
        $this->wt_report->addElement($this->current_element);
938
    }
939
940
    /**
941
     * Handle <getPersonName />
942
     * Get the name
943
     * 1. id is empty - current GEDCOM record
944
     * 2. id is set with a record id
945
     *
946
     * @param array<string> $attrs an array of key value pairs for the attributes
947
     *
948
     * @return void
949
     */
950
    protected function getPersonNameStartHandler(array $attrs): void
951
    {
952
        $id    = '';
953
        $match = [];
954
        if (empty($attrs['id'])) {
955
            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
956
                $id = $match[1];
957
            }
958
        } elseif (preg_match('/\$(.+)/', $attrs['id'], $match)) {
959
            if (isset($this->vars[$match[1]]['id'])) {
960
                $id = $this->vars[$match[1]]['id'];
961
            }
962
        } elseif (preg_match('/@(.+)/', $attrs['id'], $match)) {
963
            $gmatch = [];
964
            if (preg_match("/\d $match[1] @([^@]+)@/", $this->gedrec, $gmatch)) {
965
                $id = $gmatch[1];
966
            }
967
        } else {
968
            $id = $attrs['id'];
969
        }
970
        if (!empty($id)) {
971
            $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
972
            if ($record === null) {
973
                return;
974
            }
975
            if (!$record->canShowName()) {
976
                $this->current_element->addText(I18N::translate('Private'));
977
            } else {
978
                $name = $record->fullName();
979
                $name = strip_tags($name);
980
                if (!empty($attrs['truncate'])) {
981
                    $name = Str::limit($name, (int) $attrs['truncate'], I18N::translate('…'));
982
                } else {
983
                    $addname = (string) $record->alternateName();
984
                    $addname = strip_tags($addname);
985
                    if (!empty($addname)) {
986
                        $name .= ' ' . $addname;
987
                    }
988
                }
989
                $this->current_element->addText(trim($name));
990
            }
991
        }
992
    }
993
994
    /**
995
     * Handle <gedcomValue />
996
     *
997
     * @param array<string> $attrs
998
     *
999
     * @return void
1000
     */
1001
    protected function gedcomValueStartHandler(array $attrs): void
1002
    {
1003
        $id    = '';
1004
        $match = [];
1005
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1006
            $id = $match[1];
1007
        }
1008
1009
        if (isset($attrs['newline']) && $attrs['newline'] === '1') {
1010
            $useBreak = '1';
1011
        } else {
1012
            $useBreak = '0';
1013
        }
1014
1015
        $tag = $attrs['tag'];
1016
        if (!empty($tag)) {
1017
            if ($tag === '@desc') {
1018
                $value = $this->desc;
1019
                $value = trim($value);
1020
                $this->current_element->addText($value);
1021
            }
1022
            if ($tag === '@id') {
1023
                $this->current_element->addText($id);
1024
            } else {
1025
                $tag = str_replace('@fact', $this->fact, $tag);
1026
                if (empty($attrs['level'])) {
1027
                    $level = (int) explode(' ', trim($this->gedrec))[0];
1028
                    if ($level === 0) {
1029
                        $level++;
1030
                    }
1031
                } else {
1032
                    $level = (int) $attrs['level'];
1033
                }
1034
                $tags  = preg_split('/[: ]/', $tag);
1035
                $value = $this->getGedcomValue($tag, $level, $this->gedrec);
1036
                switch (end($tags)) {
1037
                    case 'DATE':
1038
                        $tmp   = new Date($value);
1039
                        $value = strip_tags($tmp->display());
1040
                        break;
1041
                    case 'PLAC':
1042
                        $tmp   = new Place($value, $this->tree);
1043
                        $value = $tmp->shortName();
1044
                        break;
1045
                }
1046
                if ($useBreak === '1') {
1047
                    // Insert <br> when multiple dates exist.
1048
                    // This works around a TCPDF bug that incorrectly wraps RTL dates on LTR pages
1049
                    $value = str_replace('(', '<br>(', $value);
1050
                    $value = str_replace('<span dir="ltr"><br>', '<br><span dir="ltr">', $value);
1051
                    $value = str_replace('<span dir="rtl"><br>', '<br><span dir="rtl">', $value);
1052
                    if (substr($value, 0, 4) === '<br>') {
1053
                        $value = substr($value, 4);
1054
                    }
1055
                }
1056
                $tmp = explode(':', $tag);
1057
                if (in_array(end($tmp), ['NOTE', 'TEXT'], true)) {
1058
                    if ($this->tree->getPreference('FORMAT_TEXT') === 'markdown') {
1059
                        $value = strip_tags(Registry::markdownFactory()->markdown($value, $this->tree), ['br']);
1060
                    } else {
1061
                        $value = strip_tags(Registry::markdownFactory()->autolink($value, $this->tree), ['br']);
1062
                    }
1063
                    $value = strtr($value, [MarkdownFactory::BREAK => ' ']);
1064
                }
1065
1066
                if (!empty($attrs['truncate'])) {
1067
                    $value = Str::limit($value, (int) $attrs['truncate'], I18N::translate('…'));
1068
                }
1069
                $this->current_element->addText($value);
1070
            }
1071
        }
1072
    }
1073
1074
    /**
1075
     * Handle <repeatTag>
1076
     *
1077
     * @param array<string> $attrs
1078
     *
1079
     * @return void
1080
     */
1081
    protected function repeatTagStartHandler(array $attrs): void
1082
    {
1083
        $this->process_repeats++;
1084
        if ($this->process_repeats > 1) {
1085
            return;
1086
        }
1087
1088
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
1089
        $this->repeats         = [];
1090
        $this->repeat_bytes    = xml_get_current_line_number($this->parser);
1091
1092
        $tag = $attrs['tag'] ?? '';
1093
        if (!empty($tag)) {
1094
            if ($tag === '@desc') {
1095
                $value = $this->desc;
1096
                $value = trim($value);
1097
                $this->current_element->addText($value);
1098
            } else {
1099
                $tag   = str_replace('@fact', $this->fact, $tag);
1100
                $tags  = explode(':', $tag);
1101
                $level = (int) explode(' ', trim($this->gedrec))[0];
1102
                if ($level === 0) {
1103
                    $level++;
1104
                }
1105
                $subrec = $this->gedrec;
1106
                $t      = $tag;
1107
                $count  = count($tags);
1108
                $i      = 0;
1109
                while ($i < $count) {
1110
                    $t = $tags[$i];
1111
                    if (!empty($t)) {
1112
                        if ($i < ($count - 1)) {
1113
                            $subrec = self::getSubRecord($level, "$level $t", $subrec);
1114
                            if (empty($subrec)) {
1115
                                $level--;
1116
                                $subrec = self::getSubRecord($level, "@ $t", $this->gedrec);
1117
                                if (empty($subrec)) {
1118
                                    return;
1119
                                }
1120
                            }
1121
                        }
1122
                        $level++;
1123
                    }
1124
                    $i++;
1125
                }
1126
                $level--;
1127
                $count = preg_match_all("/$level $t(.*)/", $subrec, $match, PREG_SET_ORDER);
1128
                $i     = 0;
1129
                while ($i < $count) {
1130
                    $i++;
1131
                    // Privacy check - is this a link, and are we allowed to view the linked object?
1132
                    $subrecord = self::getSubRecord($level, "$level $t", $subrec, $i);
1133
                    if (preg_match('/^\d ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@/', $subrecord, $xref_match)) {
1134
                        $linked_object = Registry::gedcomRecordFactory()->make($xref_match[1], $this->tree);
1135
                        if ($linked_object && !$linked_object->canShow()) {
1136
                            continue;
1137
                        }
1138
                    }
1139
                    $this->repeats[] = $subrecord;
1140
                }
1141
            }
1142
        }
1143
    }
1144
1145
    /**
1146
     * Handle </repeatTag>
1147
     *
1148
     * @return void
1149
     */
1150
    protected function repeatTagEndHandler(): void
1151
    {
1152
        $this->process_repeats--;
1153
        if ($this->process_repeats > 0) {
1154
            return;
1155
        }
1156
1157
        // Check if there is anything to repeat
1158
        if (count($this->repeats) > 0) {
1159
            // No need to load them if not used...
1160
1161
            $lineoffset = 0;
1162
            foreach ($this->repeats_stack as $rep) {
1163
                $lineoffset += $rep[1];
1164
            }
1165
            //-- read the xml from the file
1166
            $lines = file($this->report);
1167
            while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<RepeatTag')) {
1168
                $lineoffset--;
1169
            }
1170
            $lineoffset++;
1171
            $reportxml = "<tempdoc>\n";
1172
            $line_nr   = $lineoffset + $this->repeat_bytes;
1173
            // RepeatTag Level counter
1174
            $count = 1;
1175
            while (0 < $count) {
1176
                if (str_contains($lines[$line_nr], '<RepeatTag')) {
1177
                    $count++;
1178
                } elseif (str_contains($lines[$line_nr], '</RepeatTag')) {
1179
                    $count--;
1180
                }
1181
                if (0 < $count) {
1182
                    $reportxml .= $lines[$line_nr];
1183
                }
1184
                $line_nr++;
1185
            }
1186
            // No need to drag this
1187
            unset($lines);
1188
            $reportxml .= "</tempdoc>\n";
1189
            // Save original values
1190
            $this->parser_stack[] = $this->parser;
1191
            $oldgedrec            = $this->gedrec;
1192
            foreach ($this->repeats as $gedrec) {
1193
                $this->gedrec  = $gedrec;
1194
                $repeat_parser = xml_parser_create();
1195
                $this->parser  = $repeat_parser;
0 ignored issues
show
Documentation Bug introduced by
It seems like $repeat_parser can also be of type resource. However, the property $parser is declared as type XMLParser. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1196
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
1197
1198
                xml_set_element_handler(
1199
                    $repeat_parser,
1200
                    function ($parser, string $name, array $attrs): void {
1201
                        $this->startElement($parser, $name, $attrs);
1202
                    },
1203
                    function ($parser, string $name): void {
1204
                        $this->endElement($parser, $name);
1205
                    }
1206
                );
1207
1208
                xml_set_character_data_handler(
1209
                    $repeat_parser,
1210
                    function ($parser, string $data): void {
1211
                        $this->characterData($parser, $data);
1212
                    }
1213
                );
1214
1215
                if (!xml_parse($repeat_parser, $reportxml, true)) {
1216
                    throw new DomainException(sprintf(
1217
                        'RepeatTagEHandler XML error: %s at line %d',
1218
                        xml_error_string(xml_get_error_code($repeat_parser)),
1219
                        xml_get_current_line_number($repeat_parser)
1220
                    ));
1221
                }
1222
                xml_parser_free($repeat_parser);
1223
            }
1224
            // Restore original values
1225
            $this->gedrec = $oldgedrec;
1226
            $this->parser = array_pop($this->parser_stack);
1227
        }
1228
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
1229
    }
1230
1231
    /**
1232
     * Variable lookup
1233
     * Retrieve predefined variables :
1234
     * @ desc GEDCOM fact description, example:
1235
     *        1 EVEN This is a description
1236
     * @ fact GEDCOM fact tag, such as BIRT, DEAT etc.
1237
     * $ I18N::translate('....')
1238
     * $ language_settings[]
1239
     *
1240
     * @param array<string> $attrs an array of key value pairs for the attributes
1241
     *
1242
     * @return void
1243
     */
1244
    protected function varStartHandler(array $attrs): void
1245
    {
1246
        if (empty($attrs['var'])) {
1247
            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));
1248
        }
1249
1250
        $var = $attrs['var'];
1251
        // SetVar element preset variables
1252
        if (!empty($this->vars[$var]['id'])) {
1253
            $var = $this->vars[$var]['id'];
1254
        } else {
1255
            $tfact = $this->fact;
1256
            if (($this->fact === 'EVEN' || $this->fact === 'FACT') && $this->type !== '') {
1257
                // Use :
1258
                // n TYPE This text if string
1259
                $tfact = $this->type;
1260
            } else {
1261
                foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) {
1262
                    $element = Registry::elementFactory()->make($record_type . ':' . $this->fact);
1263
1264
                    if (!$element instanceof UnknownElement) {
1265
                        $tfact = $element->label();
1266
                        break;
1267
                    }
1268
                }
1269
            }
1270
1271
            $var = strtr($var, ['@desc' => $this->desc, '@fact' => $tfact]);
1272
1273
            if (preg_match('/^I18N::number\((.+)\)$/', $var, $match)) {
1274
                $var = I18N::number((int) $match[1]);
1275
            } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $var, $match)) {
1276
                $var = I18N::translate($match[1]);
1277
            } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $var, $match)) {
1278
                $var = I18N::translateContext($match[1], $match[2]);
1279
            }
1280
        }
1281
        // Check if variable is set as a date and reformat the date
1282
        if (isset($attrs['date'])) {
1283
            if ($attrs['date'] === '1') {
1284
                $g   = new Date($var);
1285
                $var = $g->display();
1286
            }
1287
        }
1288
        $this->current_element->addText($var);
1289
        $this->text = $var; // Used for title/descriptio
1290
    }
1291
1292
    /**
1293
     * Handle <facts>
1294
     *
1295
     * @param array<string> $attrs
1296
     *
1297
     * @return void
1298
     */
1299
    protected function factsStartHandler(array $attrs): void
1300
    {
1301
        $this->process_repeats++;
1302
        if ($this->process_repeats > 1) {
1303
            return;
1304
        }
1305
1306
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
1307
        $this->repeats         = [];
1308
        $this->repeat_bytes    = xml_get_current_line_number($this->parser);
1309
1310
        $id    = '';
1311
        $match = [];
1312
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1313
            $id = $match[1];
1314
        }
1315
        $tag = '';
1316
        if (isset($attrs['ignore'])) {
1317
            $tag .= $attrs['ignore'];
1318
        }
1319
        if (preg_match('/\$(.+)/', $tag, $match)) {
1320
            $tag = $this->vars[$match[1]]['id'];
1321
        }
1322
1323
        $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1324
        if (empty($attrs['diff']) && !empty($id)) {
1325
            $facts = $record->facts([], true);
1326
            $this->repeats = [];
1327
            $nonfacts      = explode(',', $tag);
1328
            foreach ($facts as $fact) {
1329
                $tag = explode(':', $fact->tag())[1];
1330
1331
                if (!in_array($tag, $nonfacts, true)) {
1332
                    $this->repeats[] = $fact->gedcom();
1333
                }
1334
            }
1335
        } else {
1336
            foreach ($record->facts() as $fact) {
1337
                if (($fact->isPendingAddition() || $fact->isPendingDeletion()) && !str_ends_with($fact->tag(), ':CHAN')) {
1338
                    $this->repeats[] = $fact->gedcom();
1339
                }
1340
            }
1341
        }
1342
    }
1343
1344
    /**
1345
     * Handle </facts>
1346
     *
1347
     * @return void
1348
     */
1349
    protected function factsEndHandler(): void
1350
    {
1351
        $this->process_repeats--;
1352
        if ($this->process_repeats > 0) {
1353
            return;
1354
        }
1355
1356
        // Check if there is anything to repeat
1357
        if (count($this->repeats) > 0) {
1358
            $line       = xml_get_current_line_number($this->parser) - 1;
1359
            $lineoffset = 0;
1360
            foreach ($this->repeats_stack as $rep) {
1361
                $lineoffset += $rep[1];
1362
            }
1363
1364
            //-- read the xml from the file
1365
            $lines = file($this->report);
1366
            while ($lineoffset + $this->repeat_bytes > 0 && !str_contains($lines[$lineoffset + $this->repeat_bytes], '<Facts ')) {
1367
                $lineoffset--;
1368
            }
1369
            $lineoffset++;
1370
            $reportxml = "<tempdoc>\n";
1371
            $i         = $line + $lineoffset;
1372
            $line_nr   = $this->repeat_bytes + $lineoffset;
1373
            while ($line_nr < $i) {
1374
                $reportxml .= $lines[$line_nr];
1375
                $line_nr++;
1376
            }
1377
            // No need to drag this
1378
            unset($lines);
1379
            $reportxml .= "</tempdoc>\n";
1380
            // Save original values
1381
            $this->parser_stack[] = $this->parser;
1382
            $oldgedrec            = $this->gedrec;
1383
            $count                = count($this->repeats);
1384
            $i                    = 0;
1385
            while ($i < $count) {
1386
                $this->gedrec = $this->repeats[$i];
1387
                $this->fact   = '';
1388
                $this->desc   = '';
1389
                if (preg_match('/1 (\w+)(.*)/', $this->gedrec, $match)) {
1390
                    $this->fact = $match[1];
1391
                    if ($this->fact === 'EVEN' || $this->fact === 'FACT') {
1392
                        $tmatch = [];
1393
                        if (preg_match('/2 TYPE (.+)/', $this->gedrec, $tmatch)) {
1394
                            $this->type = trim($tmatch[1]);
1395
                        } else {
1396
                            $this->type = ' ';
1397
                        }
1398
                    }
1399
                    $this->desc = trim($match[2]);
1400
                    $this->desc .= self::getCont(2, $this->gedrec);
1401
                }
1402
                $repeat_parser = xml_parser_create();
1403
                $this->parser  = $repeat_parser;
0 ignored issues
show
Documentation Bug introduced by
It seems like $repeat_parser can also be of type resource. However, the property $parser is declared as type XMLParser. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1404
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
1405
1406
                xml_set_element_handler(
1407
                    $repeat_parser,
1408
                    function ($parser, string $name, array $attrs): void {
1409
                        $this->startElement($parser, $name, $attrs);
1410
                    },
1411
                    function ($parser, string $name): void {
1412
                        $this->endElement($parser, $name);
1413
                    }
1414
                );
1415
1416
                xml_set_character_data_handler(
1417
                    $repeat_parser,
1418
                    function ($parser, string $data): void {
1419
                        $this->characterData($parser, $data);
1420
                    }
1421
                );
1422
1423
                if (!xml_parse($repeat_parser, $reportxml, true)) {
1424
                    throw new DomainException(sprintf(
1425
                        'FactsEHandler XML error: %s at line %d',
1426
                        xml_error_string(xml_get_error_code($repeat_parser)),
1427
                        xml_get_current_line_number($repeat_parser)
1428
                    ));
1429
                }
1430
                xml_parser_free($repeat_parser);
1431
                $i++;
1432
            }
1433
            // Restore original values
1434
            $this->parser = array_pop($this->parser_stack);
1435
            $this->gedrec = $oldgedrec;
1436
        }
1437
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
1438
    }
1439
1440
    /**
1441
     * Setting upp or changing variables in the XML
1442
     * The XML variable name and value is stored in $this->vars
1443
     *
1444
     * @param array<string> $attrs an array of key value pairs for the attributes
1445
     *
1446
     * @return void
1447
     */
1448
    protected function setVarStartHandler(array $attrs): void
1449
    {
1450
        if (empty($attrs['name'])) {
1451
            throw new DomainException('REPORT ERROR var: The attribute "name" is missing or not set in the XML file');
1452
        }
1453
1454
        $name  = $attrs['name'];
1455
        $value = $attrs['value'];
1456
        $match = [];
1457
        // Current GEDCOM record strings
1458
        if ($value === '@ID') {
1459
            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1460
                $value = $match[1];
1461
            }
1462
        } elseif ($value === '@fact') {
1463
            $value = $this->fact;
1464
        } elseif ($value === '@desc') {
1465
            $value = $this->desc;
1466
        } elseif ($value === '@generation') {
1467
            $value = (string) $this->generation;
1468
        } elseif (preg_match("/@(\w+)/", $value, $match)) {
1469
            $gmatch = [];
1470
            if (preg_match("/\d $match[1] (.+)/", $this->gedrec, $gmatch)) {
1471
                $value = str_replace('@', '', trim($gmatch[1]));
1472
            }
1473
        }
1474
        if (preg_match("/\\$(\w+)/", $name, $match)) {
1475
            $name = $this->vars["'" . $match[1] . "'"]['id'];
1476
        }
1477
        $count = preg_match_all("/\\$(\w+)/", $value, $match, PREG_SET_ORDER);
1478
        $i     = 0;
1479
        while ($i < $count) {
1480
            $t     = $this->vars[$match[$i][1]]['id'];
1481
            $value = preg_replace('/\$' . $match[$i][1] . '/', $t, $value, 1);
1482
            $i++;
1483
        }
1484
        if (preg_match('/^I18N::number\((.+)\)$/', $value, $match)) {
1485
            $value = I18N::number((int) $match[1]);
1486
        } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $value, $match)) {
1487
            $value = I18N::translate($match[1]);
1488
        } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $value, $match)) {
1489
            $value = I18N::translateContext($match[1], $match[2]);
1490
        }
1491
1492
        // Arithmetic functions
1493
        if (preg_match("/(\d+)\s*([-+*\/])\s*(\d+)/", $value, $match)) {
1494
            // Create an expression language with the functions used by our reports.
1495
            $expression_provider  = new ReportExpressionLanguageProvider();
1496
            $expression_cache     = new NullAdapter();
1497
            $expression_language  = new ExpressionLanguage($expression_cache, [$expression_provider]);
1498
1499
            $value = (string) $expression_language->evaluate($value);
1500
        }
1501
1502
        if (str_contains($value, '@')) {
1503
            $value = '';
1504
        }
1505
        $this->vars[$name]['id'] = $value;
1506
    }
1507
1508
    /**
1509
     * Handle <if>
1510
     *
1511
     * @param array<string> $attrs
1512
     *
1513
     * @return void
1514
     */
1515
    protected function ifStartHandler(array $attrs): void
1516
    {
1517
        if ($this->process_ifs > 0) {
1518
            $this->process_ifs++;
1519
1520
            return;
1521
        }
1522
1523
        $condition = $attrs['condition'];
1524
        $condition = $this->substituteVars($condition, true);
1525
        $condition = str_replace([
1526
            ' LT ',
1527
            ' GT ',
1528
        ], [
1529
            '<',
1530
            '>',
1531
        ], $condition);
1532
        // Replace the first occurrence only once of @fact:DATE or in any other combinations to the current fact, such as BIRT
1533
        $condition = str_replace('@fact:', $this->fact . ':', $condition);
1534
        $match     = [];
1535
        $count     = preg_match_all("/@([\w:.]+)/", $condition, $match, PREG_SET_ORDER);
1536
        $i         = 0;
1537
        while ($i < $count) {
1538
            $id    = $match[$i][1];
1539
            $value = '""';
1540
            if ($id === 'ID') {
1541
                if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1542
                    $value = "'" . $match[1] . "'";
1543
                }
1544
            } elseif ($id === 'fact') {
1545
                $value = '"' . $this->fact . '"';
1546
            } elseif ($id === 'desc') {
1547
                $value = '"' . addslashes($this->desc) . '"';
1548
            } elseif ($id === 'generation') {
1549
                $value = '"' . $this->generation . '"';
1550
            } else {
1551
                $level = (int) explode(' ', trim($this->gedrec))[0];
1552
                if ($level === 0) {
1553
                    $level++;
1554
                }
1555
                $value = $this->getGedcomValue($id, $level, $this->gedrec);
1556
                if (empty($value)) {
1557
                    $level++;
1558
                    $value = $this->getGedcomValue($id, $level, $this->gedrec);
1559
                }
1560
                $value = preg_replace('/^@(' . Gedcom::REGEX_XREF . ')@$/', '$1', $value);
1561
                $value = '"' . addslashes($value) . '"';
1562
            }
1563
            $condition = str_replace("@$id", $value, $condition);
1564
            $i++;
1565
        }
1566
1567
        // Create an expression language with the functions used by our reports.
1568
        $expression_provider  = new ReportExpressionLanguageProvider();
1569
        $expression_cache     = new NullAdapter();
1570
        $expression_language  = new ExpressionLanguage($expression_cache, [$expression_provider]);
1571
1572
        $ret = $expression_language->evaluate($condition);
1573
1574
        if (!$ret) {
1575
            $this->process_ifs++;
1576
        }
1577
    }
1578
1579
    /**
1580
     * Handle </if>
1581
     *
1582
     * @return void
1583
     */
1584
    protected function ifEndHandler(): void
1585
    {
1586
        if ($this->process_ifs > 0) {
1587
            $this->process_ifs--;
1588
        }
1589
    }
1590
1591
    /**
1592
     * Handle <footnote>
1593
     * Collect the Footnote links
1594
     * GEDCOM Records that are protected by Privacy setting will be ignored
1595
     *
1596
     * @param array<string> $attrs
1597
     *
1598
     * @return void
1599
     */
1600
    protected function footnoteStartHandler(array $attrs): void
1601
    {
1602
        $id = '';
1603
        if (preg_match('/[0-9] (.+) @(.+)@/', $this->gedrec, $match)) {
1604
            $id = $match[2];
1605
        }
1606
        $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1607
        if ($record && $record->canShow()) {
1608
            $this->print_data_stack[] = $this->print_data;
1609
            $this->print_data         = true;
1610
            $style                    = '';
1611
            if (!empty($attrs['style'])) {
1612
                $style = $attrs['style'];
1613
            }
1614
            $this->footnote_element = $this->current_element;
1615
            $this->current_element  = $this->report_root->createFootnote($style);
1616
        } else {
1617
            $this->print_data       = false;
1618
            $this->process_footnote = false;
1619
        }
1620
    }
1621
1622
    /**
1623
     * Handle </footnote>
1624
     * Print the collected Footnote data
1625
     *
1626
     * @return void
1627
     */
1628
    protected function footnoteEndHandler(): void
1629
    {
1630
        if ($this->process_footnote) {
1631
            $this->print_data = array_pop($this->print_data_stack);
1632
            $temp             = trim($this->current_element->getValue());
1633
            if (strlen($temp) > 3) {
1634
                $this->wt_report->addElement($this->current_element);
1635
            }
1636
            $this->current_element = $this->footnote_element;
1637
        } else {
1638
            $this->process_footnote = true;
1639
        }
1640
    }
1641
1642
    /**
1643
     * Handle <footnoteTexts />
1644
     *
1645
     * @return void
1646
     */
1647
    protected function footnoteTextsStartHandler(): void
1648
    {
1649
        $temp = 'footnotetexts';
1650
        $this->wt_report->addElement($temp);
1651
    }
1652
1653
    /**
1654
     * XML element Forced line break handler - HTML code
1655
     *
1656
     * @return void
1657
     */
1658
    protected function brStartHandler(): void
1659
    {
1660
        if ($this->print_data && $this->process_gedcoms === 0) {
1661
            $this->current_element->addText('<br>');
1662
        }
1663
    }
1664
1665
    /**
1666
     * Handle <sp />
1667
     * Forced space
1668
     *
1669
     * @return void
1670
     */
1671
    protected function spStartHandler(): void
1672
    {
1673
        if ($this->print_data && $this->process_gedcoms === 0) {
1674
            $this->current_element->addText(' ');
1675
        }
1676
    }
1677
1678
    /**
1679
     * Handle <highlightedImage />
1680
     *
1681
     * @param array<string> $attrs
1682
     *
1683
     * @return void
1684
     */
1685
    protected function highlightedImageStartHandler(array $attrs): void
1686
    {
1687
        $id = '';
1688
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1689
            $id = $match[1];
1690
        }
1691
1692
        // Position the top corner of this box on the page
1693
        $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION);
1694
1695
        // Position the left corner of this box on the page
1696
        $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION);
1697
1698
        // string Align the image in left, center, right (or empty to use x/y position).
1699
        $align = $attrs['align'] ?? '';
1700
1701
        // string Next Line should be T:next to the image, N:next line
1702
        $ln = $attrs['ln'] ?? 'T';
1703
1704
        // Width, height (or both).
1705
        $width  = (float) ($attrs['width'] ?? 0.0);
1706
        $height = (float) ($attrs['height'] ?? 0.0);
1707
1708
        $person     = Registry::individualFactory()->make($id, $this->tree);
1709
        $media_file = $person->findHighlightedMediaFile();
1710
1711
        if ($media_file instanceof MediaFile && $media_file->fileExists()) {
1712
            $image      = imagecreatefromstring($media_file->fileContents());
1713
            $attributes = [imagesx($image), imagesy($image)];
1714
1715
            if ($width > 0 && $height == 0) {
1716
                $perc   = $width / $attributes[0];
1717
                $height = round($attributes[1] * $perc);
1718
            } elseif ($height > 0 && $width == 0) {
1719
                $perc  = $height / $attributes[1];
1720
                $width = round($attributes[0] * $perc);
1721
            } else {
1722
                $width  = (float) $attributes[0];
1723
                $height = (float) $attributes[1];
1724
            }
1725
            $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln);
1726
            $this->wt_report->addElement($image);
1727
        }
1728
    }
1729
1730
    /**
1731
     * Handle <image/>
1732
     *
1733
     * @param array<string> $attrs
1734
     *
1735
     * @return void
1736
     */
1737
    protected function imageStartHandler(array $attrs): void
1738
    {
1739
        // Position the top corner of this box on the page. the default is the current position
1740
        $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION);
1741
1742
        // mixed Position the left corner of this box on the page. the default is the current position
1743
        $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION);
1744
1745
        // string Align the image in left, center, right (or empty to use x/y position).
1746
        $align = $attrs['align'] ?? '';
1747
1748
        // string Next Line should be T:next to the image, N:next line
1749
        $ln = $attrs['ln'] ?? 'T';
1750
1751
        // Width, height (or both).
1752
        $width  = (float) ($attrs['width'] ?? 0.0);
1753
        $height = (float) ($attrs['height'] ?? 0.0);
1754
1755
        $file = $attrs['file'] ?? '';
1756
1757
        if ($file === '@FILE') {
1758
            $match = [];
1759
            if (preg_match("/\d OBJE @(.+)@/", $this->gedrec, $match)) {
1760
                $mediaobject = Registry::mediaFactory()->make($match[1], $this->tree);
1761
                $media_file  = $mediaobject->firstImageFile();
1762
1763
                if ($media_file instanceof MediaFile && $media_file->fileExists()) {
1764
                    $image      = imagecreatefromstring($media_file->fileContents());
1765
                    $attributes = [imagesx($image), imagesy($image)];
1766
1767
                    if ($width > 0 && $height == 0) {
1768
                        $perc   = $width / $attributes[0];
1769
                        $height = round($attributes[1] * $perc);
1770
                    } elseif ($height > 0 && $width == 0) {
1771
                        $perc  = $height / $attributes[1];
1772
                        $width = round($attributes[0] * $perc);
1773
                    } else {
1774
                        $width  = (float) $attributes[0];
1775
                        $height = (float) $attributes[1];
1776
                    }
1777
                    $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln);
1778
                    $this->wt_report->addElement($image);
1779
                }
1780
            }
1781
        } elseif (file_exists($file) && preg_match('/(jpg|jpeg|png|gif)$/i', $file)) {
1782
            $size = getimagesize($file);
1783
            if ($width > 0 && $height == 0) {
1784
                $perc   = $width / $size[0];
1785
                $height = round($size[1] * $perc);
1786
            } elseif ($height > 0 && $width == 0) {
1787
                $perc  = $height / $size[1];
1788
                $width = round($size[0] * $perc);
1789
            } else {
1790
                $width  = $size[0];
1791
                $height = $size[1];
1792
            }
1793
            $image = $this->report_root->createImage($file, $left, $top, $width, $height, $align, $ln);
1794
            $this->wt_report->addElement($image);
1795
        }
1796
    }
1797
1798
    /**
1799
     * Handle <line>
1800
     *
1801
     * @param array<string> $attrs
1802
     *
1803
     * @return void
1804
     */
1805
    protected function lineStartHandler(array $attrs): void
1806
    {
1807
        // Start horizontal position, current position (default)
1808
        $x1 = ReportBaseElement::CURRENT_POSITION;
1809
        if (isset($attrs['x1'])) {
1810
            if ($attrs['x1'] === '0') {
1811
                $x1 = 0;
1812
            } elseif ($attrs['x1'] === '.') {
1813
                $x1 = ReportBaseElement::CURRENT_POSITION;
1814
            } elseif (!empty($attrs['x1'])) {
1815
                $x1 = (float) $attrs['x1'];
1816
            }
1817
        }
1818
        // Start vertical position, current position (default)
1819
        $y1 = ReportBaseElement::CURRENT_POSITION;
1820
        if (isset($attrs['y1'])) {
1821
            if ($attrs['y1'] === '0') {
1822
                $y1 = 0;
1823
            } elseif ($attrs['y1'] === '.') {
1824
                $y1 = ReportBaseElement::CURRENT_POSITION;
1825
            } elseif (!empty($attrs['y1'])) {
1826
                $y1 = (float) $attrs['y1'];
1827
            }
1828
        }
1829
        // End horizontal position, maximum width (default)
1830
        $x2 = ReportBaseElement::CURRENT_POSITION;
1831
        if (isset($attrs['x2'])) {
1832
            if ($attrs['x2'] === '0') {
1833
                $x2 = 0;
1834
            } elseif ($attrs['x2'] === '.') {
1835
                $x2 = ReportBaseElement::CURRENT_POSITION;
1836
            } elseif (!empty($attrs['x2'])) {
1837
                $x2 = (float) $attrs['x2'];
1838
            }
1839
        }
1840
        // End vertical position
1841
        $y2 = ReportBaseElement::CURRENT_POSITION;
1842
        if (isset($attrs['y2'])) {
1843
            if ($attrs['y2'] === '0') {
1844
                $y2 = 0;
1845
            } elseif ($attrs['y2'] === '.') {
1846
                $y2 = ReportBaseElement::CURRENT_POSITION;
1847
            } elseif (!empty($attrs['y2'])) {
1848
                $y2 = (float) $attrs['y2'];
1849
            }
1850
        }
1851
1852
        $line = $this->report_root->createLine($x1, $y1, $x2, $y2);
1853
        $this->wt_report->addElement($line);
1854
    }
1855
1856
    /**
1857
     * Handle <list>
1858
     *
1859
     * @param array<string> $attrs
1860
     *
1861
     * @return void
1862
     */
1863
    protected function listStartHandler(array $attrs): void
1864
    {
1865
        $this->process_repeats++;
1866
        if ($this->process_repeats > 1) {
1867
            return;
1868
        }
1869
1870
        $match = [];
1871
        if (isset($attrs['sortby'])) {
1872
            $sortby = $attrs['sortby'];
1873
            if (preg_match("/\\$(\w+)/", $sortby, $match)) {
1874
                $sortby = $this->vars[$match[1]]['id'];
1875
                $sortby = trim($sortby);
1876
            }
1877
        } else {
1878
            $sortby = 'NAME';
1879
        }
1880
1881
        $listname = $attrs['list'] ?? 'individual';
1882
1883
        // Some filters/sorts can be applied using SQL, while others require PHP
1884
        switch ($listname) {
1885
            case 'pending':
1886
                $this->list = DB::table('change')
1887
                    ->whereIn('change_id', function (Builder $query): void {
1888
                        $query->select([new Expression('MAX(change_id)')])
1889
                            ->from('change')
1890
                            ->where('gedcom_id', '=', $this->tree->id())
1891
                            ->where('status', '=', 'pending')
1892
                            ->groupBy(['xref']);
1893
                    })
1894
                    ->get()
1895
                    ->map(fn (object $row): ?GedcomRecord => Registry::gedcomRecordFactory()->make($row->xref, $this->tree, $row->new_gedcom ?: $row->old_gedcom))
1896
                    ->filter()
1897
                    ->all();
1898
                break;
1899
1900
            case 'individual':
1901
                $query = DB::table('individuals')
1902
                    ->where('i_file', '=', $this->tree->id())
1903
                    ->select(['i_id AS xref', 'i_gedcom AS gedcom'])
1904
                    ->distinct();
1905
1906
                foreach ($attrs as $attr => $value) {
1907
                    if (str_starts_with($attr, 'filter') && $value !== '') {
1908
                        $value = $this->substituteVars($value, false);
1909
                        // Convert the various filters into SQL
1910
                        if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
1911
                            $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void {
1912
                                $join
1913
                                    ->on($attr . '.d_gid', '=', 'i_id')
1914
                                    ->on($attr . '.d_file', '=', 'i_file');
1915
                            });
1916
1917
                            $query->where($attr . '.d_fact', '=', $match[1]);
1918
1919
                            $date = new Date($match[3]);
1920
1921
                            if ($match[2] === 'LTE') {
1922
                                $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay());
1923
                            } else {
1924
                                $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay());
1925
                            }
1926
1927
                            // This filter has been fully processed
1928
                            unset($attrs[$attr]);
1929
                        } elseif (preg_match('/^NAME CONTAINS (.+)$/', $value, $match)) {
1930
                            $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void {
1931
                                $join
1932
                                    ->on($attr . '.n_id', '=', 'i_id')
1933
                                    ->on($attr . '.n_file', '=', 'i_file');
1934
                            });
1935
                            // Search the DB only if there is any name supplied
1936
                            $names = explode(' ', $match[1]);
1937
                            foreach ($names as $name) {
1938
                                $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%');
1939
                            }
1940
1941
                            // This filter has been fully processed
1942
                            unset($attrs[$attr]);
1943
                        } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) {
1944
                            // Convert newline escape sequences to actual new lines
1945
                            $match[1] = str_replace('\n', "\n", $match[1]);
1946
1947
                            $query->where('i_gedcom', 'LIKE', $match[1]);
1948
1949
                            // This filter has been fully processed
1950
                            unset($attrs[$attr]);
1951
                        } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) {
1952
                            // Don't unset this filter. This is just initial filtering for performance
1953
                            $query
1954
                                ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void {
1955
                                    $join
1956
                                        ->on($attr . 'a.pl_file', '=', 'i_file')
1957
                                        ->on($attr . 'a.pl_gid', '=', 'i_id');
1958
                                })
1959
                                ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void {
1960
                                    $join
1961
                                        ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file')
1962
                                        ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id');
1963
                                })
1964
                                ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%');
1965
                        } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) {
1966
                            // Don't unset this filter. This is just initial filtering for performance
1967
                            $match[3] = strtr($match[3], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
1968
                            $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%';
1969
                            $query->where('i_gedcom', 'LIKE', $like);
1970
                        } elseif (preg_match('/^(\w+) CONTAINS (.*)$/', $value, $match)) {
1971
                            // Don't unset this filter. This is just initial filtering for performance
1972
                            $match[2] = strtr($match[2], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
1973
                            $like = "%\n1 " . $match[1] . '%' . $match[2] . '%';
1974
                            $query->where('i_gedcom', 'LIKE', $like);
1975
                        }
1976
                    }
1977
                }
1978
1979
                $this->list = [];
1980
1981
                foreach ($query->get() as $row) {
1982
                    $this->list[$row->xref] = Registry::individualFactory()->make($row->xref, $this->tree, $row->gedcom);
1983
                }
1984
                break;
1985
1986
            case 'family':
1987
                $query = DB::table('families')
1988
                    ->where('f_file', '=', $this->tree->id())
1989
                    ->select(['f_id AS xref', 'f_gedcom AS gedcom'])
1990
                    ->distinct();
1991
1992
                foreach ($attrs as $attr => $value) {
1993
                    if (str_starts_with($attr, 'filter') && $value !== '') {
1994
                        $value = $this->substituteVars($value, false);
1995
                        // Convert the various filters into SQL
1996
                        if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
1997
                            $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void {
1998
                                $join
1999
                                    ->on($attr . '.d_gid', '=', 'f_id')
2000
                                    ->on($attr . '.d_file', '=', 'f_file');
2001
                            });
2002
2003
                            $query->where($attr . '.d_fact', '=', $match[1]);
2004
2005
                            $date = new Date($match[3]);
2006
2007
                            if ($match[2] === 'LTE') {
2008
                                $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay());
2009
                            } else {
2010
                                $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay());
2011
                            }
2012
2013
                            // This filter has been fully processed
2014
                            unset($attrs[$attr]);
2015
                        } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) {
2016
                            // Convert newline escape sequences to actual new lines
2017
                            $match[1] = str_replace('\n', "\n", $match[1]);
2018
2019
                            $query->where('f_gedcom', 'LIKE', $match[1]);
2020
2021
                            // This filter has been fully processed
2022
                            unset($attrs[$attr]);
2023
                        } elseif (preg_match('/^NAME CONTAINS (.*)$/', $value, $match)) {
2024
                            if ($sortby === 'NAME' || $match[1] !== '') {
2025
                                $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2026
                                    $join
2027
                                        ->on($attr . '.n_file', '=', 'f_file')
2028
                                        ->where(static function (Builder $query): void {
2029
                                            $query
2030
                                                ->whereColumn('n_id', '=', 'f_husb')
2031
                                                ->orWhereColumn('n_id', '=', 'f_wife');
2032
                                        });
2033
                                });
2034
                                // Search the DB only if there is any name supplied
2035
                                if ($match[1] != '') {
2036
                                    $names = explode(' ', $match[1]);
2037
                                    foreach ($names as $name) {
2038
                                        $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%');
2039
                                    }
2040
                                }
2041
                            }
2042
2043
                            // This filter has been fully processed
2044
                            unset($attrs[$attr]);
2045
                        } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) {
2046
                            // Don't unset this filter. This is just initial filtering for performance
2047
                            $query
2048
                                ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void {
2049
                                    $join
2050
                                        ->on($attr . 'a.pl_file', '=', 'f_file')
2051
                                        ->on($attr . 'a.pl_gid', '=', 'f_id');
2052
                                })
2053
                                ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void {
2054
                                    $join
2055
                                        ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file')
2056
                                        ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id');
2057
                                })
2058
                                ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%');
2059
                        } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) {
2060
                            // Don't unset this filter. This is just initial filtering for performance
2061
                            $match[3] = strtr($match[3], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2062
                            $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%';
2063
                            $query->where('f_gedcom', 'LIKE', $like);
2064
                        } elseif (preg_match('/^(\w+) CONTAINS (.+)$/', $value, $match)) {
2065
                            // Don't unset this filter. This is just initial filtering for performance
2066
                            $match[2] = strtr($match[2], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2067
                            $like = "%\n1 " . $match[1] . '%' . $match[2] . '%';
2068
                            $query->where('f_gedcom', 'LIKE', $like);
2069
                        }
2070
                    }
2071
                }
2072
2073
                $this->list = [];
2074
2075
                foreach ($query->get() as $row) {
2076
                    $this->list[$row->xref] = Registry::familyFactory()->make($row->xref, $this->tree, $row->gedcom);
2077
                }
2078
                break;
2079
2080
            default:
2081
                throw new DomainException('Invalid list name: ' . $listname);
2082
        }
2083
2084
        $filters  = [];
2085
        $filters2 = [];
2086
        if (isset($attrs['filter1']) && count($this->list) > 0) {
2087
            foreach ($attrs as $key => $value) {
2088
                if (preg_match("/filter(\d)/", $key)) {
2089
                    $condition = $value;
2090
                    if (preg_match("/@(\w+)/", $condition, $match)) {
2091
                        $id    = $match[1];
2092
                        $value = "''";
2093
                        if ($id === 'ID') {
2094
                            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
2095
                                $value = "'" . $match[1] . "'";
2096
                            }
2097
                        } elseif ($id === 'fact') {
2098
                            $value = "'" . $this->fact . "'";
2099
                        } elseif ($id === 'desc') {
2100
                            $value = "'" . $this->desc . "'";
2101
                        } elseif (preg_match("/\d $id (.+)/", $this->gedrec, $match)) {
2102
                            $value = "'" . str_replace('@', '', trim($match[1])) . "'";
2103
                        }
2104
                        $condition = preg_replace("/@$id/", $value, $condition);
2105
                    }
2106
                    //-- handle regular expressions
2107
                    if (preg_match("/([A-Z:]+)\s*([^\s]+)\s*(.+)/", $condition, $match)) {
2108
                        $tag  = trim($match[1]);
2109
                        $expr = trim($match[2]);
2110
                        $val  = trim($match[3]);
2111
                        if (preg_match("/\\$(\w+)/", $val, $match)) {
2112
                            $val = $this->vars[$match[1]]['id'];
2113
                            $val = trim($val);
2114
                        }
2115
                        if ($val !== '') {
2116
                            $searchstr = '';
2117
                            $tags      = explode(':', $tag);
2118
                            //-- only limit to a level number if we are specifically looking at a level
2119
                            if (count($tags) > 1) {
2120
                                $level = 1;
2121
                                $t = 'XXXX';
2122
                                foreach ($tags as $t) {
2123
                                    if (!empty($searchstr)) {
2124
                                        $searchstr .= "[^\n]*(\n[2-9][^\n]*)*\n";
2125
                                    }
2126
                                    //-- search for both EMAIL and _EMAIL... silly double gedcom standard
2127
                                    if ($t === 'EMAIL' || $t === '_EMAIL') {
2128
                                        $t = '_?EMAIL';
2129
                                    }
2130
                                    $searchstr .= $level . ' ' . $t;
2131
                                    $level++;
2132
                                }
2133
                            } else {
2134
                                if ($tag === 'EMAIL' || $tag === '_EMAIL') {
2135
                                    $tag = '_?EMAIL';
2136
                                }
2137
                                $t         = $tag;
2138
                                $searchstr = '1 ' . $tag;
2139
                            }
2140
                            switch ($expr) {
2141
                                case 'CONTAINS':
2142
                                    if ($t === 'PLAC') {
2143
                                        $searchstr .= "[^\n]*[, ]*" . $val;
2144
                                    } else {
2145
                                        $searchstr .= "[^\n]*" . $val;
2146
                                    }
2147
                                    $filters[] = $searchstr;
2148
                                    break;
2149
                                default:
2150
                                    $filters2[] = [
2151
                                        'tag'  => $tag,
2152
                                        'expr' => $expr,
2153
                                        'val'  => $val,
2154
                                    ];
2155
                                    break;
2156
                            }
2157
                        }
2158
                    }
2159
                }
2160
            }
2161
        }
2162
        //-- apply other filters to the list that could not be added to the search string
2163
        if ($filters !== []) {
2164
            foreach ($this->list as $key => $record) {
2165
                foreach ($filters as $filter) {
2166
                    if (!preg_match('/' . $filter . '/i', $record->privatizeGedcom(Auth::accessLevel($this->tree)))) {
2167
                        unset($this->list[$key]);
2168
                        break;
2169
                    }
2170
                }
2171
            }
2172
        }
2173
        if ($filters2 !== []) {
2174
            $mylist = [];
2175
            foreach ($this->list as $indi) {
2176
                $key  = $indi->xref();
2177
                $grec = $indi->privatizeGedcom(Auth::accessLevel($this->tree));
2178
                $keep = true;
2179
                foreach ($filters2 as $filter) {
2180
                    if ($keep) {
2181
                        $tag  = $filter['tag'];
2182
                        $expr = $filter['expr'];
2183
                        $val  = $filter['val'];
2184
                        if ($val === "''") {
2185
                            $val = '';
2186
                        }
2187
                        $tags = explode(':', $tag);
2188
                        $t    = end($tags);
2189
                        $v    = $this->getGedcomValue($tag, 1, $grec);
2190
                        //-- check for EMAIL and _EMAIL (silly double gedcom standard :P)
2191
                        if ($t === 'EMAIL' && empty($v)) {
2192
                            $tag  = str_replace('EMAIL', '_EMAIL', $tag);
2193
                            $tags = explode(':', $tag);
2194
                            $t    = end($tags);
2195
                            $v    = self::getSubRecord(1, $tag, $grec);
2196
                        }
2197
2198
                        switch ($expr) {
2199
                            case 'GTE':
2200
                                if ($t === 'DATE') {
2201
                                    $date1 = new Date($v);
2202
                                    $date2 = new Date($val);
2203
                                    $keep  = (Date::compare($date1, $date2) >= 0);
2204
                                } elseif ($val >= $v) {
2205
                                    $keep = true;
2206
                                }
2207
                                break;
2208
                            case 'LTE':
2209
                                if ($t === 'DATE') {
2210
                                    $date1 = new Date($v);
2211
                                    $date2 = new Date($val);
2212
                                    $keep  = (Date::compare($date1, $date2) <= 0);
2213
                                } elseif ($val >= $v) {
2214
                                    $keep = true;
2215
                                }
2216
                                break;
2217
                            default:
2218
                                if ($v == $val) {
2219
                                    $keep = true;
2220
                                } else {
2221
                                    $keep = false;
2222
                                }
2223
                                break;
2224
                        }
2225
                    }
2226
                }
2227
                if ($keep) {
2228
                    $mylist[$key] = $indi;
2229
                }
2230
            }
2231
            $this->list = $mylist;
2232
        }
2233
2234
        switch ($sortby) {
2235
            case 'NAME':
2236
                uasort($this->list, GedcomRecord::nameComparator());
2237
                break;
2238
            case 'CHAN':
2239
                uasort($this->list, GedcomRecord::lastChangeComparator());
2240
                break;
2241
            case 'BIRT:DATE':
2242
                uasort($this->list, Individual::birthDateComparator());
2243
                break;
2244
            case 'DEAT:DATE':
2245
                uasort($this->list, Individual::deathDateComparator());
2246
                break;
2247
            case 'MARR:DATE':
2248
                uasort($this->list, Family::marriageDateComparator());
2249
                break;
2250
            default:
2251
                // unsorted or already sorted by SQL
2252
                break;
2253
        }
2254
2255
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
2256
        $this->repeat_bytes    = xml_get_current_line_number($this->parser) + 1;
2257
    }
2258
2259
    /**
2260
     * Handle </list>
2261
     *
2262
     * @return void
2263
     */
2264
    protected function listEndHandler(): void
2265
    {
2266
        $this->process_repeats--;
2267
        if ($this->process_repeats > 0) {
2268
            return;
2269
        }
2270
2271
        // Check if there is any list
2272
        if (count($this->list) > 0) {
2273
            $lineoffset = 0;
2274
            foreach ($this->repeats_stack as $rep) {
2275
                $lineoffset += $rep[1];
2276
            }
2277
            //-- read the xml from the file
2278
            $lines = file($this->report);
2279
            while ((!str_contains($lines[$lineoffset + $this->repeat_bytes], '<List')) && (($lineoffset + $this->repeat_bytes) > 0)) {
2280
                $lineoffset--;
2281
            }
2282
            $lineoffset++;
2283
            $reportxml = "<tempdoc>\n";
2284
            $line_nr   = $lineoffset + $this->repeat_bytes;
2285
            // List Level counter
2286
            $count = 1;
2287
            while (0 < $count) {
2288
                if (str_contains($lines[$line_nr], '<List')) {
2289
                    $count++;
2290
                } elseif (str_contains($lines[$line_nr], '</List')) {
2291
                    $count--;
2292
                }
2293
                if (0 < $count) {
2294
                    $reportxml .= $lines[$line_nr];
2295
                }
2296
                $line_nr++;
2297
            }
2298
            // No need to drag this
2299
            unset($lines);
2300
            $reportxml .= '</tempdoc>';
2301
            // Save original values
2302
            $this->parser_stack[] = $this->parser;
2303
            $oldgedrec            = $this->gedrec;
2304
2305
            $this->list_total   = count($this->list);
2306
            $this->list_private = 0;
2307
            foreach ($this->list as $record) {
2308
                if ($record->canShow()) {
2309
                    $this->gedrec = $record->privatizeGedcom(Auth::accessLevel($record->tree()));
2310
                    //-- start the sax parser
2311
                    $repeat_parser = xml_parser_create();
2312
                    $this->parser  = $repeat_parser;
0 ignored issues
show
Documentation Bug introduced by
It seems like $repeat_parser can also be of type resource. However, the property $parser is declared as type XMLParser. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2313
                    xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
2314
2315
                    xml_set_element_handler(
2316
                        $repeat_parser,
2317
                        function ($parser, string $name, array $attrs): void {
2318
                            $this->startElement($parser, $name, $attrs);
2319
                        },
2320
                        function ($parser, string $name): void {
2321
                            $this->endElement($parser, $name);
2322
                        }
2323
                    );
2324
2325
                    xml_set_character_data_handler(
2326
                        $repeat_parser,
2327
                        function ($parser, string $data): void {
2328
                            $this->characterData($parser, $data);
2329
                        }
2330
                    );
2331
2332
                    if (!xml_parse($repeat_parser, $reportxml, true)) {
2333
                        throw new DomainException(sprintf(
2334
                            'ListEHandler XML error: %s at line %d',
2335
                            xml_error_string(xml_get_error_code($repeat_parser)),
2336
                            xml_get_current_line_number($repeat_parser)
2337
                        ));
2338
                    }
2339
                    xml_parser_free($repeat_parser);
2340
                } else {
2341
                    $this->list_private++;
2342
                }
2343
            }
2344
            $this->list   = [];
2345
            $this->parser = array_pop($this->parser_stack);
2346
            $this->gedrec = $oldgedrec;
2347
        }
2348
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
2349
    }
2350
2351
    /**
2352
     * Handle <listTotal>
2353
     * Prints the total number of records in a list
2354
     * The total number is collected from <list> and <relatives>
2355
     *
2356
     * @return void
2357
     */
2358
    protected function listTotalStartHandler(): void
2359
    {
2360
        if ($this->list_private == 0) {
2361
            $this->current_element->addText((string) $this->list_total);
2362
        } else {
2363
            $this->current_element->addText(($this->list_total - $this->list_private) . ' / ' . $this->list_total);
2364
        }
2365
    }
2366
2367
    /**
2368
     * Handle <relatives>
2369
     *
2370
     * @param array<string> $attrs
2371
     *
2372
     * @return void
2373
     */
2374
    protected function relativesStartHandler(array $attrs): void
2375
    {
2376
        $this->process_repeats++;
2377
        if ($this->process_repeats > 1) {
2378
            return;
2379
        }
2380
2381
        $sortby = $attrs['sortby'] ?? 'NAME';
2382
2383
        $match = [];
2384
        if (preg_match("/\\$(\w+)/", $sortby, $match)) {
2385
            $sortby = $this->vars[$match[1]]['id'];
2386
            $sortby = trim($sortby);
2387
        }
2388
2389
        $maxgen = -1;
2390
        if (isset($attrs['maxgen'])) {
2391
            $maxgen = (int) $attrs['maxgen'];
2392
        }
2393
2394
        $group = $attrs['group'] ?? 'child-family';
2395
2396
        if (preg_match("/\\$(\w+)/", $group, $match)) {
2397
            $group = $this->vars[$match[1]]['id'];
2398
            $group = trim($group);
2399
        }
2400
2401
        $id = $attrs['id'] ?? '';
2402
2403
        if (preg_match("/\\$(\w+)/", $id, $match)) {
2404
            $id = $this->vars[$match[1]]['id'];
2405
            $id = trim($id);
2406
        }
2407
2408
        $this->list = [];
2409
        $person     = Registry::individualFactory()->make($id, $this->tree);
2410
        if ($person instanceof Individual) {
2411
            $this->list[$id] = $person;
2412
            switch ($group) {
2413
                case 'child-family':
2414
                    foreach ($person->childFamilies() as $family) {
2415
                        foreach ($family->spouses() as $spouse) {
2416
                            $this->list[$spouse->xref()] = $spouse;
2417
                        }
2418
2419
                        foreach ($family->children() as $child) {
2420
                            $this->list[$child->xref()] = $child;
2421
                        }
2422
                    }
2423
                    break;
2424
                case 'spouse-family':
2425
                    foreach ($person->spouseFamilies() as $family) {
2426
                        foreach ($family->spouses() as $spouse) {
2427
                            $this->list[$spouse->xref()] = $spouse;
2428
                        }
2429
2430
                        foreach ($family->children() as $child) {
2431
                            $this->list[$child->xref()] = $child;
2432
                        }
2433
                    }
2434
                    break;
2435
                case 'direct-ancestors':
2436
                    $this->addAncestors($this->list, $id, false, $maxgen);
2437
                    break;
2438
                case 'ancestors':
2439
                    $this->addAncestors($this->list, $id, true, $maxgen);
2440
                    break;
2441
                case 'descendants':
2442
                    $this->list[$id]->generation = 1;
2443
                    $this->addDescendancy($this->list, $id, false, $maxgen);
2444
                    break;
2445
                case 'all':
2446
                    $this->addAncestors($this->list, $id, true, $maxgen);
2447
                    $this->addDescendancy($this->list, $id, true, $maxgen);
2448
                    break;
2449
            }
2450
        }
2451
2452
        switch ($sortby) {
2453
            case 'NAME':
2454
                uasort($this->list, GedcomRecord::nameComparator());
2455
                break;
2456
            case 'BIRT:DATE':
2457
                uasort($this->list, Individual::birthDateComparator());
2458
                break;
2459
            case 'DEAT:DATE':
2460
                uasort($this->list, Individual::deathDateComparator());
2461
                break;
2462
            case 'generation':
2463
                $newarray = [];
2464
                reset($this->list);
2465
                $genCounter = 1;
2466
                while (count($newarray) < count($this->list)) {
2467
                    foreach ($this->list as $key => $value) {
2468
                        $this->generation = $value->generation;
2469
                        if ($this->generation == $genCounter) {
2470
                            $newarray[$key] = (object) ['generation' => $this->generation];
2471
                        }
2472
                    }
2473
                    $genCounter++;
2474
                }
2475
                $this->list = $newarray;
2476
                break;
2477
            default:
2478
                // unsorted
2479
                break;
2480
        }
2481
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
2482
        $this->repeat_bytes    = xml_get_current_line_number($this->parser) + 1;
2483
    }
2484
2485
    /**
2486
     * Handle </relatives>
2487
     *
2488
     * @return void
2489
     */
2490
    protected function relativesEndHandler(): void
2491
    {
2492
        $this->process_repeats--;
2493
        if ($this->process_repeats > 0) {
2494
            return;
2495
        }
2496
2497
        // Check if there is any relatives
2498
        if (count($this->list) > 0) {
2499
            $lineoffset = 0;
2500
            foreach ($this->repeats_stack as $rep) {
2501
                $lineoffset += $rep[1];
2502
            }
2503
            //-- read the xml from the file
2504
            $lines = file($this->report);
2505
            while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<Relatives') && $lineoffset + $this->repeat_bytes > 0) {
2506
                $lineoffset--;
2507
            }
2508
            $lineoffset++;
2509
            $reportxml = "<tempdoc>\n";
2510
            $line_nr   = $lineoffset + $this->repeat_bytes;
2511
            // Relatives Level counter
2512
            $count = 1;
2513
            while (0 < $count) {
2514
                if (str_contains($lines[$line_nr], '<Relatives')) {
2515
                    $count++;
2516
                } elseif (str_contains($lines[$line_nr], '</Relatives')) {
2517
                    $count--;
2518
                }
2519
                if (0 < $count) {
2520
                    $reportxml .= $lines[$line_nr];
2521
                }
2522
                $line_nr++;
2523
            }
2524
            // No need to drag this
2525
            unset($lines);
2526
            $reportxml .= "</tempdoc>\n";
2527
            // Save original values
2528
            $this->parser_stack[] = $this->parser;
2529
            $oldgedrec            = $this->gedrec;
2530
2531
            $this->list_total   = count($this->list);
2532
            $this->list_private = 0;
2533
            foreach ($this->list as $xref => $value) {
2534
                if (isset($value->generation)) {
2535
                    $this->generation = $value->generation;
2536
                }
2537
                $tmp          = Registry::gedcomRecordFactory()->make((string) $xref, $this->tree);
2538
                $this->gedrec = $tmp->privatizeGedcom(Auth::accessLevel($this->tree));
2539
2540
                $repeat_parser = xml_parser_create();
2541
                $this->parser  = $repeat_parser;
0 ignored issues
show
Documentation Bug introduced by
It seems like $repeat_parser can also be of type resource. However, the property $parser is declared as type XMLParser. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
2542
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
2543
2544
                xml_set_element_handler(
2545
                    $repeat_parser,
2546
                    function ($parser, string $name, array $attrs): void {
2547
                        $this->startElement($parser, $name, $attrs);
2548
                    },
2549
                    function ($parser, string $name): void {
2550
                        $this->endElement($parser, $name);
2551
                    }
2552
                );
2553
2554
                xml_set_character_data_handler(
2555
                    $repeat_parser,
2556
                    function ($parser, string $data): void {
2557
                        $this->characterData($parser, $data);
2558
                    }
2559
                );
2560
2561
                if (!xml_parse($repeat_parser, $reportxml, true)) {
2562
                    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)));
2563
                }
2564
                xml_parser_free($repeat_parser);
2565
            }
2566
            // Clean up the list array
2567
            $this->list   = [];
2568
            $this->parser = array_pop($this->parser_stack);
2569
            $this->gedrec = $oldgedrec;
2570
        }
2571
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
2572
    }
2573
2574
    /**
2575
     * Handle <generation />
2576
     * Prints the number of generations
2577
     *
2578
     * @return void
2579
     */
2580
    protected function generationStartHandler(): void
2581
    {
2582
        $this->current_element->addText((string) $this->generation);
2583
    }
2584
2585
    /**
2586
     * Handle <newPage />
2587
     * Has to be placed in an element (header, body or footer)
2588
     *
2589
     * @return void
2590
     */
2591
    protected function newPageStartHandler(): void
2592
    {
2593
        $temp = 'addpage';
2594
        $this->wt_report->addElement($temp);
2595
    }
2596
2597
    /**
2598
     * Handle </title>
2599
     *
2600
     * @return void
2601
     */
2602
    protected function titleEndHandler(): void
2603
    {
2604
        $this->report_root->addTitle($this->text);
2605
    }
2606
2607
    /**
2608
     * Handle </description>
2609
     *
2610
     * @return void
2611
     */
2612
    protected function descriptionEndHandler(): void
2613
    {
2614
        $this->report_root->addDescription($this->text);
2615
    }
2616
2617
    /**
2618
     * Create a list of all descendants.
2619
     *
2620
     * @param array<Individual> $list
2621
     * @param string            $pid
2622
     * @param bool              $parents
2623
     * @param int               $generations
2624
     *
2625
     * @return void
2626
     */
2627
    private function addDescendancy(&$list, $pid, $parents = false, $generations = -1): void
2628
    {
2629
        $person = Registry::individualFactory()->make($pid, $this->tree);
2630
        if ($person === null) {
2631
            return;
2632
        }
2633
        if (!isset($list[$pid])) {
2634
            $list[$pid] = $person;
2635
        }
2636
        if (!isset($list[$pid]->generation)) {
2637
            $list[$pid]->generation = 0;
2638
        }
2639
        foreach ($person->spouseFamilies() as $family) {
2640
            if ($parents) {
2641
                $husband = $family->husband();
2642
                $wife    = $family->wife();
2643
                if ($husband) {
2644
                    $list[$husband->xref()] = $husband;
2645
                    if (isset($list[$pid]->generation)) {
2646
                        $list[$husband->xref()]->generation = $list[$pid]->generation - 1;
2647
                    } else {
2648
                        $list[$husband->xref()]->generation = 1;
2649
                    }
2650
                }
2651
                if ($wife) {
2652
                    $list[$wife->xref()] = $wife;
2653
                    if (isset($list[$pid]->generation)) {
2654
                        $list[$wife->xref()]->generation = $list[$pid]->generation - 1;
2655
                    } else {
2656
                        $list[$wife->xref()]->generation = 1;
2657
                    }
2658
                }
2659
            }
2660
2661
            $children = $family->children();
2662
2663
            foreach ($children as $child) {
2664
                if ($child) {
2665
                    $list[$child->xref()] = $child;
2666
2667
                    if (isset($list[$pid]->generation)) {
2668
                        $list[$child->xref()]->generation = $list[$pid]->generation + 1;
2669
                    } else {
2670
                        $list[$child->xref()]->generation = 2;
2671
                    }
2672
                }
2673
            }
2674
            if ($generations == -1 || $list[$pid]->generation + 1 < $generations) {
2675
                foreach ($children as $child) {
2676
                    $this->addDescendancy($list, $child->xref(), $parents, $generations); // recurse on the childs family
2677
                }
2678
            }
2679
        }
2680
    }
2681
2682
    /**
2683
     * Create a list of all ancestors.
2684
     *
2685
     * @param array<Individual> $list
2686
     * @param string            $pid
2687
     * @param bool              $children
2688
     * @param int               $generations
2689
     *
2690
     * @return void
2691
     */
2692
    private function addAncestors(array &$list, string $pid, bool $children = false, int $generations = -1): void
2693
    {
2694
        $genlist                = [$pid];
2695
        $list[$pid]->generation = 1;
2696
        while (count($genlist) > 0) {
2697
            $id = array_shift($genlist);
2698
            if (str_starts_with($id, 'empty')) {
2699
                continue; // id can be something like “empty7”
2700
            }
2701
            $person = Registry::individualFactory()->make($id, $this->tree);
2702
            foreach ($person->childFamilies() as $family) {
2703
                $husband = $family->husband();
2704
                $wife    = $family->wife();
2705
                if ($husband) {
2706
                    $list[$husband->xref()]             = $husband;
2707
                    $list[$husband->xref()]->generation = $list[$id]->generation + 1;
2708
                }
2709
                if ($wife) {
2710
                    $list[$wife->xref()]             = $wife;
2711
                    $list[$wife->xref()]->generation = $list[$id]->generation + 1;
2712
                }
2713
                if ($generations == -1 || $list[$id]->generation + 1 < $generations) {
2714
                    if ($husband) {
2715
                        $genlist[] = $husband->xref();
2716
                    }
2717
                    if ($wife) {
2718
                        $genlist[] = $wife->xref();
2719
                    }
2720
                }
2721
                if ($children) {
2722
                    foreach ($family->children() as $child) {
2723
                        $list[$child->xref()] = $child;
2724
                        $list[$child->xref()]->generation = $list[$id]->generation ?? 1;
2725
                    }
2726
                }
2727
            }
2728
        }
2729
    }
2730
2731
    /**
2732
     * get gedcom tag value
2733
     *
2734
     * @param string $tag    The tag to find, use : to delineate subtags
2735
     * @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
2736
     * @param string $gedrec The gedcom record to get the value from
2737
     *
2738
     * @return string the value of a gedcom tag from the given gedcom record
2739
     */
2740
    private function getGedcomValue(string $tag, int $level, string $gedrec): string
2741
    {
2742
        if ($gedrec === '') {
2743
            return '';
2744
        }
2745
        $tags      = explode(':', $tag);
2746
        $origlevel = $level;
2747
        if ($level === 0) {
2748
            $level = 1 + (int) $gedrec[0];
2749
        }
2750
2751
        $subrec = $gedrec;
2752
        $t = 'XXXX';
2753
        foreach ($tags as $t) {
2754
            $lastsubrec = $subrec;
2755
            $subrec     = self::getSubRecord($level, "$level $t", $subrec);
2756
            if (empty($subrec) && $origlevel == 0) {
2757
                $level--;
2758
                $subrec = self::getSubRecord($level, "$level $t", $lastsubrec);
2759
            }
2760
            if (empty($subrec)) {
2761
                if ($t === 'TITL') {
2762
                    $subrec = self::getSubRecord($level, "$level ABBR", $lastsubrec);
2763
                    if (!empty($subrec)) {
2764
                        $t = 'ABBR';
2765
                    }
2766
                }
2767
                if ($subrec === '') {
2768
                    if ($level > 0) {
2769
                        $level--;
2770
                    }
2771
                    $subrec = self::getSubRecord($level, "@ $t", $gedrec);
2772
                    if ($subrec === '') {
2773
                        return '';
2774
                    }
2775
                }
2776
            }
2777
            $level++;
2778
        }
2779
        $level--;
2780
        $ct = preg_match("/$level $t(.*)/", $subrec, $match);
2781
        if ($ct === 0) {
2782
            $ct = preg_match("/$level @.+@ (.+)/", $subrec, $match);
2783
        }
2784
        if ($ct === 0) {
2785
            $ct = preg_match("/@ $t (.+)/", $subrec, $match);
2786
        }
2787
        if ($ct > 0) {
2788
            $value = trim($match[1]);
2789
            if ($t === 'NOTE' && preg_match('/^@(.+)@$/', $value, $match)) {
2790
                $note = Registry::noteFactory()->make($match[1], $this->tree);
2791
                if ($note instanceof Note) {
2792
                    $value = $note->getNote();
2793
                } else {
2794
                    //-- set the value to the id without the @
2795
                    $value = $match[1];
2796
                }
2797
            }
2798
            if ($level !== 0 || $t !== 'NOTE') {
2799
                $value .= self::getCont($level + 1, $subrec);
2800
            }
2801
2802
            if ($tag === 'NAME' || $tag === '_MARNM' || $tag === '_AKA') {
2803
                return strtr($value, ['/' => '']);
2804
            }
2805
2806
            return $value;
2807
        }
2808
2809
        return '';
2810
    }
2811
2812
    /**
2813
     * Replace variable identifiers with their values.
2814
     *
2815
     * @param string $expression An expression such as "$foo == 123"
2816
     * @param bool   $quote      Whether to add quotation marks
2817
     *
2818
     * @return string
2819
     */
2820
    private function substituteVars($expression, $quote): string
2821
    {
2822
        return preg_replace_callback(
2823
            '/\$(\w+)/',
2824
            function (array $matches) use ($quote): string {
2825
                if (isset($this->vars[$matches[1]]['id'])) {
2826
                    if ($quote) {
2827
                        return "'" . addcslashes($this->vars[$matches[1]]['id'], "'") . "'";
2828
                    }
2829
2830
                    return $this->vars[$matches[1]]['id'];
2831
                }
2832
2833
                Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1]));
2834
2835
                return '$' . $matches[1];
2836
            },
2837
            $expression
2838
        );
2839
    }
2840
}
2841