ReportParserGenerate::docStartHandler()   F
last analyzed

Complexity

Conditions 28
Paths > 20000

Size

Total Lines 85
Code Lines 48

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 28
eloc 48
nc 524288
nop 1
dl 0
loc 85
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 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
1223
                if (PHP_MAJOR_VERSION < 8) {
1224
                    xml_parser_free($repeat_parser);
1225
                }
1226
            }
1227
            // Restore original values
1228
            $this->gedrec = $oldgedrec;
1229
            $this->parser = array_pop($this->parser_stack);
1230
        }
1231
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
1232
    }
1233
1234
    /**
1235
     * Variable lookup
1236
     * Retrieve predefined variables :
1237
     * @ desc GEDCOM fact description, example:
1238
     *        1 EVEN This is a description
1239
     * @ fact GEDCOM fact tag, such as BIRT, DEAT etc.
1240
     * $ I18N::translate('....')
1241
     * $ language_settings[]
1242
     *
1243
     * @param array<string> $attrs an array of key value pairs for the attributes
1244
     *
1245
     * @return void
1246
     */
1247
    protected function varStartHandler(array $attrs): void
1248
    {
1249
        if (empty($attrs['var'])) {
1250
            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));
1251
        }
1252
1253
        $var = $attrs['var'];
1254
        // SetVar element preset variables
1255
        if (!empty($this->vars[$var]['id'])) {
1256
            $var = $this->vars[$var]['id'];
1257
        } else {
1258
            $tfact = $this->fact;
1259
            if (($this->fact === 'EVEN' || $this->fact === 'FACT') && $this->type !== '') {
1260
                // Use :
1261
                // n TYPE This text if string
1262
                $tfact = $this->type;
1263
            } else {
1264
                foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) {
1265
                    $element = Registry::elementFactory()->make($record_type . ':' . $this->fact);
1266
1267
                    if (!$element instanceof UnknownElement) {
1268
                        $tfact = $element->label();
1269
                        break;
1270
                    }
1271
                }
1272
            }
1273
1274
            $var = strtr($var, ['@desc' => $this->desc, '@fact' => $tfact]);
1275
1276
            if (preg_match('/^I18N::number\((.+)\)$/', $var, $match)) {
1277
                $var = I18N::number((int) $match[1]);
1278
            } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $var, $match)) {
1279
                $var = I18N::translate($match[1]);
1280
            } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $var, $match)) {
1281
                $var = I18N::translateContext($match[1], $match[2]);
1282
            }
1283
        }
1284
        // Check if variable is set as a date and reformat the date
1285
        if (isset($attrs['date'])) {
1286
            if ($attrs['date'] === '1') {
1287
                $g   = new Date($var);
1288
                $var = $g->display();
1289
            }
1290
        }
1291
        $this->current_element->addText($var);
1292
        $this->text = $var; // Used for title/descriptio
1293
    }
1294
1295
    /**
1296
     * Handle <facts>
1297
     *
1298
     * @param array<string> $attrs
1299
     *
1300
     * @return void
1301
     */
1302
    protected function factsStartHandler(array $attrs): void
1303
    {
1304
        $this->process_repeats++;
1305
        if ($this->process_repeats > 1) {
1306
            return;
1307
        }
1308
1309
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
1310
        $this->repeats         = [];
1311
        $this->repeat_bytes    = xml_get_current_line_number($this->parser);
1312
1313
        $id    = '';
1314
        $match = [];
1315
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1316
            $id = $match[1];
1317
        }
1318
        $tag = '';
1319
        if (isset($attrs['ignore'])) {
1320
            $tag .= $attrs['ignore'];
1321
        }
1322
        if (preg_match('/\$(.+)/', $tag, $match)) {
1323
            $tag = $this->vars[$match[1]]['id'];
1324
        }
1325
1326
        $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1327
        if (empty($attrs['diff']) && !empty($id)) {
1328
            $facts = $record->facts([], true);
1329
            $this->repeats = [];
1330
            $nonfacts      = explode(',', $tag);
1331
            foreach ($facts as $fact) {
1332
                $tag = explode(':', $fact->tag())[1];
1333
1334
                if (!in_array($tag, $nonfacts, true)) {
1335
                    $this->repeats[] = $fact->gedcom();
1336
                }
1337
            }
1338
        } else {
1339
            foreach ($record->facts() as $fact) {
1340
                if (($fact->isPendingAddition() || $fact->isPendingDeletion()) && !str_ends_with($fact->tag(), ':CHAN')) {
1341
                    $this->repeats[] = $fact->gedcom();
1342
                }
1343
            }
1344
        }
1345
    }
1346
1347
    /**
1348
     * Handle </facts>
1349
     *
1350
     * @return void
1351
     */
1352
    protected function factsEndHandler(): void
1353
    {
1354
        $this->process_repeats--;
1355
        if ($this->process_repeats > 0) {
1356
            return;
1357
        }
1358
1359
        // Check if there is anything to repeat
1360
        if (count($this->repeats) > 0) {
1361
            $line       = xml_get_current_line_number($this->parser) - 1;
1362
            $lineoffset = 0;
1363
            foreach ($this->repeats_stack as $rep) {
1364
                $lineoffset += $rep[1];
1365
            }
1366
1367
            //-- read the xml from the file
1368
            $lines = file($this->report);
1369
            while ($lineoffset + $this->repeat_bytes > 0 && !str_contains($lines[$lineoffset + $this->repeat_bytes], '<Facts ')) {
1370
                $lineoffset--;
1371
            }
1372
            $lineoffset++;
1373
            $reportxml = "<tempdoc>\n";
1374
            $i         = $line + $lineoffset;
1375
            $line_nr   = $this->repeat_bytes + $lineoffset;
1376
            while ($line_nr < $i) {
1377
                $reportxml .= $lines[$line_nr];
1378
                $line_nr++;
1379
            }
1380
            // No need to drag this
1381
            unset($lines);
1382
            $reportxml .= "</tempdoc>\n";
1383
            // Save original values
1384
            $this->parser_stack[] = $this->parser;
1385
            $oldgedrec            = $this->gedrec;
1386
            $count                = count($this->repeats);
1387
            $i                    = 0;
1388
            while ($i < $count) {
1389
                $this->gedrec = $this->repeats[$i];
1390
                $this->fact   = '';
1391
                $this->desc   = '';
1392
                if (preg_match('/1 (\w+)(.*)/', $this->gedrec, $match)) {
1393
                    $this->fact = $match[1];
1394
                    if ($this->fact === 'EVEN' || $this->fact === 'FACT') {
1395
                        $tmatch = [];
1396
                        if (preg_match('/2 TYPE (.+)/', $this->gedrec, $tmatch)) {
1397
                            $this->type = trim($tmatch[1]);
1398
                        } else {
1399
                            $this->type = ' ';
1400
                        }
1401
                    }
1402
                    $this->desc = trim($match[2]);
1403
                    $this->desc .= self::getCont(2, $this->gedrec);
1404
                }
1405
                $repeat_parser = xml_parser_create();
1406
                $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...
1407
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
1408
1409
                xml_set_element_handler(
1410
                    $repeat_parser,
1411
                    function ($parser, string $name, array $attrs): void {
1412
                        $this->startElement($parser, $name, $attrs);
1413
                    },
1414
                    function ($parser, string $name): void {
1415
                        $this->endElement($parser, $name);
1416
                    }
1417
                );
1418
1419
                xml_set_character_data_handler(
1420
                    $repeat_parser,
1421
                    function ($parser, string $data): void {
1422
                        $this->characterData($parser, $data);
1423
                    }
1424
                );
1425
1426
                if (!xml_parse($repeat_parser, $reportxml, true)) {
1427
                    throw new DomainException(sprintf(
1428
                        'FactsEHandler XML error: %s at line %d',
1429
                        xml_error_string(xml_get_error_code($repeat_parser)),
1430
                        xml_get_current_line_number($repeat_parser)
1431
                    ));
1432
                }
1433
1434
                if (PHP_MAJOR_VERSION < 8) {
1435
                    xml_parser_free($repeat_parser);
1436
                }
1437
1438
                $i++;
1439
            }
1440
            // Restore original values
1441
            $this->parser = array_pop($this->parser_stack);
1442
            $this->gedrec = $oldgedrec;
1443
        }
1444
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
1445
    }
1446
1447
    /**
1448
     * Setting upp or changing variables in the XML
1449
     * The XML variable name and value is stored in $this->vars
1450
     *
1451
     * @param array<string> $attrs an array of key value pairs for the attributes
1452
     *
1453
     * @return void
1454
     */
1455
    protected function setVarStartHandler(array $attrs): void
1456
    {
1457
        if (empty($attrs['name'])) {
1458
            throw new DomainException('REPORT ERROR var: The attribute "name" is missing or not set in the XML file');
1459
        }
1460
1461
        $name  = $attrs['name'];
1462
        $value = $attrs['value'];
1463
        $match = [];
1464
        // Current GEDCOM record strings
1465
        if ($value === '@ID') {
1466
            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1467
                $value = $match[1];
1468
            }
1469
        } elseif ($value === '@fact') {
1470
            $value = $this->fact;
1471
        } elseif ($value === '@desc') {
1472
            $value = $this->desc;
1473
        } elseif ($value === '@generation') {
1474
            $value = (string) $this->generation;
1475
        } elseif (preg_match("/@(\w+)/", $value, $match)) {
1476
            $gmatch = [];
1477
            if (preg_match("/\d $match[1] (.+)/", $this->gedrec, $gmatch)) {
1478
                $value = str_replace('@', '', trim($gmatch[1]));
1479
            }
1480
        }
1481
        if (preg_match("/\\$(\w+)/", $name, $match)) {
1482
            $name = $this->vars["'" . $match[1] . "'"]['id'];
1483
        }
1484
        $count = preg_match_all("/\\$(\w+)/", $value, $match, PREG_SET_ORDER);
1485
        $i     = 0;
1486
        while ($i < $count) {
1487
            $t     = $this->vars[$match[$i][1]]['id'];
1488
            $value = preg_replace('/\$' . $match[$i][1] . '/', $t, $value, 1);
1489
            $i++;
1490
        }
1491
        if (preg_match('/^I18N::number\((.+)\)$/', $value, $match)) {
1492
            $value = I18N::number((int) $match[1]);
1493
        } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $value, $match)) {
1494
            $value = I18N::translate($match[1]);
1495
        } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $value, $match)) {
1496
            $value = I18N::translateContext($match[1], $match[2]);
1497
        }
1498
1499
        // Arithmetic functions
1500
        if (preg_match("/(\d+)\s*([-+*\/])\s*(\d+)/", $value, $match)) {
1501
            // Create an expression language with the functions used by our reports.
1502
            $expression_provider  = new ReportExpressionLanguageProvider();
1503
            $expression_cache     = new NullAdapter();
1504
            $expression_language  = new ExpressionLanguage($expression_cache, [$expression_provider]);
1505
1506
            $value = (string) $expression_language->evaluate($value);
1507
        }
1508
1509
        if (str_contains($value, '@')) {
1510
            $value = '';
1511
        }
1512
        $this->vars[$name]['id'] = $value;
1513
    }
1514
1515
    /**
1516
     * Handle <if>
1517
     *
1518
     * @param array<string> $attrs
1519
     *
1520
     * @return void
1521
     */
1522
    protected function ifStartHandler(array $attrs): void
1523
    {
1524
        if ($this->process_ifs > 0) {
1525
            $this->process_ifs++;
1526
1527
            return;
1528
        }
1529
1530
        $condition = $attrs['condition'];
1531
        $condition = $this->substituteVars($condition, true);
1532
        $condition = str_replace([
1533
            ' LT ',
1534
            ' GT ',
1535
        ], [
1536
            '<',
1537
            '>',
1538
        ], $condition);
1539
        // Replace the first occurrence only once of @fact:DATE or in any other combinations to the current fact, such as BIRT
1540
        $condition = str_replace('@fact:', $this->fact . ':', $condition);
1541
        $match     = [];
1542
        $count     = preg_match_all("/@([\w:.]+)/", $condition, $match, PREG_SET_ORDER);
1543
        $i         = 0;
1544
        while ($i < $count) {
1545
            $id    = $match[$i][1];
1546
            $value = '""';
1547
            if ($id === 'ID') {
1548
                if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1549
                    $value = "'" . $match[1] . "'";
1550
                }
1551
            } elseif ($id === 'fact') {
1552
                $value = '"' . $this->fact . '"';
1553
            } elseif ($id === 'desc') {
1554
                $value = '"' . addslashes($this->desc) . '"';
1555
            } elseif ($id === 'generation') {
1556
                $value = '"' . $this->generation . '"';
1557
            } else {
1558
                $level = (int) explode(' ', trim($this->gedrec))[0];
1559
                if ($level === 0) {
1560
                    $level++;
1561
                }
1562
                $value = $this->getGedcomValue($id, $level, $this->gedrec);
1563
                if (empty($value)) {
1564
                    $level++;
1565
                    $value = $this->getGedcomValue($id, $level, $this->gedrec);
1566
                }
1567
                $value = preg_replace('/^@(' . Gedcom::REGEX_XREF . ')@$/', '$1', $value);
1568
                $value = '"' . addslashes($value) . '"';
1569
            }
1570
            $condition = str_replace("@$id", $value, $condition);
1571
            $i++;
1572
        }
1573
1574
        // Create an expression language with the functions used by our reports.
1575
        $expression_provider  = new ReportExpressionLanguageProvider();
1576
        $expression_cache     = new NullAdapter();
1577
        $expression_language  = new ExpressionLanguage($expression_cache, [$expression_provider]);
1578
1579
        $ret = $expression_language->evaluate($condition);
1580
1581
        if (!$ret) {
1582
            $this->process_ifs++;
1583
        }
1584
    }
1585
1586
    /**
1587
     * Handle </if>
1588
     *
1589
     * @return void
1590
     */
1591
    protected function ifEndHandler(): void
1592
    {
1593
        if ($this->process_ifs > 0) {
1594
            $this->process_ifs--;
1595
        }
1596
    }
1597
1598
    /**
1599
     * Handle <footnote>
1600
     * Collect the Footnote links
1601
     * GEDCOM Records that are protected by Privacy setting will be ignored
1602
     *
1603
     * @param array<string> $attrs
1604
     *
1605
     * @return void
1606
     */
1607
    protected function footnoteStartHandler(array $attrs): void
1608
    {
1609
        $id = '';
1610
        if (preg_match('/[0-9] (.+) @(.+)@/', $this->gedrec, $match)) {
1611
            $id = $match[2];
1612
        }
1613
        $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1614
        if ($record && $record->canShow()) {
1615
            $this->print_data_stack[] = $this->print_data;
1616
            $this->print_data         = true;
1617
            $style                    = '';
1618
            if (!empty($attrs['style'])) {
1619
                $style = $attrs['style'];
1620
            }
1621
            $this->footnote_element = $this->current_element;
1622
            $this->current_element  = $this->report_root->createFootnote($style);
1623
        } else {
1624
            $this->print_data       = false;
1625
            $this->process_footnote = false;
1626
        }
1627
    }
1628
1629
    /**
1630
     * Handle </footnote>
1631
     * Print the collected Footnote data
1632
     *
1633
     * @return void
1634
     */
1635
    protected function footnoteEndHandler(): void
1636
    {
1637
        if ($this->process_footnote) {
1638
            $this->print_data = array_pop($this->print_data_stack);
1639
            $temp             = trim($this->current_element->getValue());
1640
            if (strlen($temp) > 3) {
1641
                $this->wt_report->addElement($this->current_element);
1642
            }
1643
            $this->current_element = $this->footnote_element;
1644
        } else {
1645
            $this->process_footnote = true;
1646
        }
1647
    }
1648
1649
    /**
1650
     * Handle <footnoteTexts />
1651
     *
1652
     * @return void
1653
     */
1654
    protected function footnoteTextsStartHandler(): void
1655
    {
1656
        $temp = 'footnotetexts';
1657
        $this->wt_report->addElement($temp);
1658
    }
1659
1660
    /**
1661
     * XML element Forced line break handler - HTML code
1662
     *
1663
     * @return void
1664
     */
1665
    protected function brStartHandler(): void
1666
    {
1667
        if ($this->print_data && $this->process_gedcoms === 0) {
1668
            $this->current_element->addText('<br>');
1669
        }
1670
    }
1671
1672
    /**
1673
     * Handle <sp />
1674
     * Forced space
1675
     *
1676
     * @return void
1677
     */
1678
    protected function spStartHandler(): void
1679
    {
1680
        if ($this->print_data && $this->process_gedcoms === 0) {
1681
            $this->current_element->addText(' ');
1682
        }
1683
    }
1684
1685
    /**
1686
     * Handle <highlightedImage />
1687
     *
1688
     * @param array<string> $attrs
1689
     *
1690
     * @return void
1691
     */
1692
    protected function highlightedImageStartHandler(array $attrs): void
1693
    {
1694
        $id = '';
1695
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1696
            $id = $match[1];
1697
        }
1698
1699
        // Position the top corner of this box on the page
1700
        $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION);
1701
1702
        // Position the left corner of this box on the page
1703
        $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION);
1704
1705
        // string Align the image in left, center, right (or empty to use x/y position).
1706
        $align = $attrs['align'] ?? '';
1707
1708
        // string Next Line should be T:next to the image, N:next line
1709
        $ln = $attrs['ln'] ?? 'T';
1710
1711
        // Width, height (or both).
1712
        $width  = (float) ($attrs['width'] ?? 0.0);
1713
        $height = (float) ($attrs['height'] ?? 0.0);
1714
1715
        $person     = Registry::individualFactory()->make($id, $this->tree);
1716
        $media_file = $person->findHighlightedMediaFile();
1717
1718
        if ($media_file instanceof MediaFile && $media_file->fileExists()) {
1719
            $image      = imagecreatefromstring($media_file->fileContents());
1720
            $attributes = [imagesx($image), imagesy($image)];
1721
1722
            if ($width > 0 && $height == 0) {
1723
                $perc   = $width / $attributes[0];
1724
                $height = round($attributes[1] * $perc);
1725
            } elseif ($height > 0 && $width == 0) {
1726
                $perc  = $height / $attributes[1];
1727
                $width = round($attributes[0] * $perc);
1728
            } else {
1729
                $width  = (float) $attributes[0];
1730
                $height = (float) $attributes[1];
1731
            }
1732
            $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln);
1733
            $this->wt_report->addElement($image);
1734
        }
1735
    }
1736
1737
    /**
1738
     * Handle <image/>
1739
     *
1740
     * @param array<string> $attrs
1741
     *
1742
     * @return void
1743
     */
1744
    protected function imageStartHandler(array $attrs): void
1745
    {
1746
        // Position the top corner of this box on the page. the default is the current position
1747
        $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION);
1748
1749
        // mixed Position the left corner of this box on the page. the default is the current position
1750
        $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION);
1751
1752
        // string Align the image in left, center, right (or empty to use x/y position).
1753
        $align = $attrs['align'] ?? '';
1754
1755
        // string Next Line should be T:next to the image, N:next line
1756
        $ln = $attrs['ln'] ?? 'T';
1757
1758
        // Width, height (or both).
1759
        $width  = (float) ($attrs['width'] ?? 0.0);
1760
        $height = (float) ($attrs['height'] ?? 0.0);
1761
1762
        $file = $attrs['file'] ?? '';
1763
1764
        if ($file === '@FILE') {
1765
            $match = [];
1766
            if (preg_match("/\d OBJE @(.+)@/", $this->gedrec, $match)) {
1767
                $mediaobject = Registry::mediaFactory()->make($match[1], $this->tree);
1768
                $media_file  = $mediaobject->firstImageFile();
1769
1770
                if ($media_file instanceof MediaFile && $media_file->fileExists()) {
1771
                    $image      = imagecreatefromstring($media_file->fileContents());
1772
                    $attributes = [imagesx($image), imagesy($image)];
1773
1774
                    if ($width > 0 && $height == 0) {
1775
                        $perc   = $width / $attributes[0];
1776
                        $height = round($attributes[1] * $perc);
1777
                    } elseif ($height > 0 && $width == 0) {
1778
                        $perc  = $height / $attributes[1];
1779
                        $width = round($attributes[0] * $perc);
1780
                    } else {
1781
                        $width  = (float) $attributes[0];
1782
                        $height = (float) $attributes[1];
1783
                    }
1784
                    $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln);
1785
                    $this->wt_report->addElement($image);
1786
                }
1787
            }
1788
        } elseif (file_exists($file) && preg_match('/(jpg|jpeg|png|gif)$/i', $file)) {
1789
            $size = getimagesize($file);
1790
            if ($width > 0 && $height == 0) {
1791
                $perc   = $width / $size[0];
1792
                $height = round($size[1] * $perc);
1793
            } elseif ($height > 0 && $width == 0) {
1794
                $perc  = $height / $size[1];
1795
                $width = round($size[0] * $perc);
1796
            } else {
1797
                $width  = $size[0];
1798
                $height = $size[1];
1799
            }
1800
            $image = $this->report_root->createImage($file, $left, $top, $width, $height, $align, $ln);
1801
            $this->wt_report->addElement($image);
1802
        }
1803
    }
1804
1805
    /**
1806
     * Handle <line>
1807
     *
1808
     * @param array<string> $attrs
1809
     *
1810
     * @return void
1811
     */
1812
    protected function lineStartHandler(array $attrs): void
1813
    {
1814
        // Start horizontal position, current position (default)
1815
        $x1 = ReportBaseElement::CURRENT_POSITION;
1816
        if (isset($attrs['x1'])) {
1817
            if ($attrs['x1'] === '0') {
1818
                $x1 = 0;
1819
            } elseif ($attrs['x1'] === '.') {
1820
                $x1 = ReportBaseElement::CURRENT_POSITION;
1821
            } elseif (!empty($attrs['x1'])) {
1822
                $x1 = (float) $attrs['x1'];
1823
            }
1824
        }
1825
        // Start vertical position, current position (default)
1826
        $y1 = ReportBaseElement::CURRENT_POSITION;
1827
        if (isset($attrs['y1'])) {
1828
            if ($attrs['y1'] === '0') {
1829
                $y1 = 0;
1830
            } elseif ($attrs['y1'] === '.') {
1831
                $y1 = ReportBaseElement::CURRENT_POSITION;
1832
            } elseif (!empty($attrs['y1'])) {
1833
                $y1 = (float) $attrs['y1'];
1834
            }
1835
        }
1836
        // End horizontal position, maximum width (default)
1837
        $x2 = ReportBaseElement::CURRENT_POSITION;
1838
        if (isset($attrs['x2'])) {
1839
            if ($attrs['x2'] === '0') {
1840
                $x2 = 0;
1841
            } elseif ($attrs['x2'] === '.') {
1842
                $x2 = ReportBaseElement::CURRENT_POSITION;
1843
            } elseif (!empty($attrs['x2'])) {
1844
                $x2 = (float) $attrs['x2'];
1845
            }
1846
        }
1847
        // End vertical position
1848
        $y2 = ReportBaseElement::CURRENT_POSITION;
1849
        if (isset($attrs['y2'])) {
1850
            if ($attrs['y2'] === '0') {
1851
                $y2 = 0;
1852
            } elseif ($attrs['y2'] === '.') {
1853
                $y2 = ReportBaseElement::CURRENT_POSITION;
1854
            } elseif (!empty($attrs['y2'])) {
1855
                $y2 = (float) $attrs['y2'];
1856
            }
1857
        }
1858
1859
        $line = $this->report_root->createLine($x1, $y1, $x2, $y2);
1860
        $this->wt_report->addElement($line);
1861
    }
1862
1863
    /**
1864
     * Handle <list>
1865
     *
1866
     * @param array<string> $attrs
1867
     *
1868
     * @return void
1869
     */
1870
    protected function listStartHandler(array $attrs): void
1871
    {
1872
        $this->process_repeats++;
1873
        if ($this->process_repeats > 1) {
1874
            return;
1875
        }
1876
1877
        $match = [];
1878
        if (isset($attrs['sortby'])) {
1879
            $sortby = $attrs['sortby'];
1880
            if (preg_match("/\\$(\w+)/", $sortby, $match)) {
1881
                $sortby = $this->vars[$match[1]]['id'];
1882
                $sortby = trim($sortby);
1883
            }
1884
        } else {
1885
            $sortby = 'NAME';
1886
        }
1887
1888
        $listname = $attrs['list'] ?? 'individual';
1889
1890
        // Some filters/sorts can be applied using SQL, while others require PHP
1891
        switch ($listname) {
1892
            case 'pending':
1893
                $this->list = DB::table('change')
1894
                    ->whereIn('change_id', function (Builder $query): void {
1895
                        $query->select([new Expression('MAX(change_id)')])
1896
                            ->from('change')
1897
                            ->where('gedcom_id', '=', $this->tree->id())
1898
                            ->where('status', '=', 'pending')
1899
                            ->groupBy(['xref']);
1900
                    })
1901
                    ->get()
1902
                    ->map(fn (object $row): ?GedcomRecord => Registry::gedcomRecordFactory()->make($row->xref, $this->tree, $row->new_gedcom ?: $row->old_gedcom))
1903
                    ->filter()
1904
                    ->all();
1905
                break;
1906
1907
            case 'individual':
1908
                $query = DB::table('individuals')
1909
                    ->where('i_file', '=', $this->tree->id())
1910
                    ->select(['i_id AS xref', 'i_gedcom AS gedcom'])
1911
                    ->distinct();
1912
1913
                foreach ($attrs as $attr => $value) {
1914
                    if (str_starts_with($attr, 'filter') && $value !== '') {
1915
                        $value = $this->substituteVars($value, false);
1916
                        // Convert the various filters into SQL
1917
                        if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
1918
                            $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void {
1919
                                $join
1920
                                    ->on($attr . '.d_gid', '=', 'i_id')
1921
                                    ->on($attr . '.d_file', '=', 'i_file');
1922
                            });
1923
1924
                            $query->where($attr . '.d_fact', '=', $match[1]);
1925
1926
                            $date = new Date($match[3]);
1927
1928
                            if ($match[2] === 'LTE') {
1929
                                $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay());
1930
                            } else {
1931
                                $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay());
1932
                            }
1933
1934
                            // This filter has been fully processed
1935
                            unset($attrs[$attr]);
1936
                        } elseif (preg_match('/^NAME CONTAINS (.+)$/', $value, $match)) {
1937
                            $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void {
1938
                                $join
1939
                                    ->on($attr . '.n_id', '=', 'i_id')
1940
                                    ->on($attr . '.n_file', '=', 'i_file');
1941
                            });
1942
                            // Search the DB only if there is any name supplied
1943
                            $names = explode(' ', $match[1]);
1944
                            foreach ($names as $name) {
1945
                                $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%');
1946
                            }
1947
1948
                            // This filter has been fully processed
1949
                            unset($attrs[$attr]);
1950
                        } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) {
1951
                            // Convert newline escape sequences to actual new lines
1952
                            $match[1] = str_replace('\n', "\n", $match[1]);
1953
1954
                            $query->where('i_gedcom', 'LIKE', $match[1]);
1955
1956
                            // This filter has been fully processed
1957
                            unset($attrs[$attr]);
1958
                        } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) {
1959
                            // Don't unset this filter. This is just initial filtering for performance
1960
                            $query
1961
                                ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void {
1962
                                    $join
1963
                                        ->on($attr . 'a.pl_file', '=', 'i_file')
1964
                                        ->on($attr . 'a.pl_gid', '=', 'i_id');
1965
                                })
1966
                                ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void {
1967
                                    $join
1968
                                        ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file')
1969
                                        ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id');
1970
                                })
1971
                                ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%');
1972
                        } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) {
1973
                            // Don't unset this filter. This is just initial filtering for performance
1974
                            $match[3] = strtr($match[3], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
1975
                            $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%';
1976
                            $query->where('i_gedcom', 'LIKE', $like);
1977
                        } elseif (preg_match('/^(\w+) CONTAINS (.*)$/', $value, $match)) {
1978
                            // Don't unset this filter. This is just initial filtering for performance
1979
                            $match[2] = strtr($match[2], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
1980
                            $like = "%\n1 " . $match[1] . '%' . $match[2] . '%';
1981
                            $query->where('i_gedcom', 'LIKE', $like);
1982
                        }
1983
                    }
1984
                }
1985
1986
                $this->list = [];
1987
1988
                foreach ($query->get() as $row) {
1989
                    $this->list[$row->xref] = Registry::individualFactory()->make($row->xref, $this->tree, $row->gedcom);
1990
                }
1991
                break;
1992
1993
            case 'family':
1994
                $query = DB::table('families')
1995
                    ->where('f_file', '=', $this->tree->id())
1996
                    ->select(['f_id AS xref', 'f_gedcom AS gedcom'])
1997
                    ->distinct();
1998
1999
                foreach ($attrs as $attr => $value) {
2000
                    if (str_starts_with($attr, 'filter') && $value !== '') {
2001
                        $value = $this->substituteVars($value, false);
2002
                        // Convert the various filters into SQL
2003
                        if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
2004
                            $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2005
                                $join
2006
                                    ->on($attr . '.d_gid', '=', 'f_id')
2007
                                    ->on($attr . '.d_file', '=', 'f_file');
2008
                            });
2009
2010
                            $query->where($attr . '.d_fact', '=', $match[1]);
2011
2012
                            $date = new Date($match[3]);
2013
2014
                            if ($match[2] === 'LTE') {
2015
                                $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay());
2016
                            } else {
2017
                                $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay());
2018
                            }
2019
2020
                            // This filter has been fully processed
2021
                            unset($attrs[$attr]);
2022
                        } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) {
2023
                            // Convert newline escape sequences to actual new lines
2024
                            $match[1] = str_replace('\n', "\n", $match[1]);
2025
2026
                            $query->where('f_gedcom', 'LIKE', $match[1]);
2027
2028
                            // This filter has been fully processed
2029
                            unset($attrs[$attr]);
2030
                        } elseif (preg_match('/^NAME CONTAINS (.*)$/', $value, $match)) {
2031
                            if ($sortby === 'NAME' || $match[1] !== '') {
2032
                                $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2033
                                    $join
2034
                                        ->on($attr . '.n_file', '=', 'f_file')
2035
                                        ->where(static function (Builder $query): void {
2036
                                            $query
2037
                                                ->whereColumn('n_id', '=', 'f_husb')
2038
                                                ->orWhereColumn('n_id', '=', 'f_wife');
2039
                                        });
2040
                                });
2041
                                // Search the DB only if there is any name supplied
2042
                                if ($match[1] != '') {
2043
                                    $names = explode(' ', $match[1]);
2044
                                    foreach ($names as $name) {
2045
                                        $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%');
2046
                                    }
2047
                                }
2048
                            }
2049
2050
                            // This filter has been fully processed
2051
                            unset($attrs[$attr]);
2052
                        } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) {
2053
                            // Don't unset this filter. This is just initial filtering for performance
2054
                            $query
2055
                                ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void {
2056
                                    $join
2057
                                        ->on($attr . 'a.pl_file', '=', 'f_file')
2058
                                        ->on($attr . 'a.pl_gid', '=', 'f_id');
2059
                                })
2060
                                ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void {
2061
                                    $join
2062
                                        ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file')
2063
                                        ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id');
2064
                                })
2065
                                ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%');
2066
                        } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) {
2067
                            // Don't unset this filter. This is just initial filtering for performance
2068
                            $match[3] = strtr($match[3], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2069
                            $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%';
2070
                            $query->where('f_gedcom', 'LIKE', $like);
2071
                        } elseif (preg_match('/^(\w+) CONTAINS (.+)$/', $value, $match)) {
2072
                            // Don't unset this filter. This is just initial filtering for performance
2073
                            $match[2] = strtr($match[2], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2074
                            $like = "%\n1 " . $match[1] . '%' . $match[2] . '%';
2075
                            $query->where('f_gedcom', 'LIKE', $like);
2076
                        }
2077
                    }
2078
                }
2079
2080
                $this->list = [];
2081
2082
                foreach ($query->get() as $row) {
2083
                    $this->list[$row->xref] = Registry::familyFactory()->make($row->xref, $this->tree, $row->gedcom);
2084
                }
2085
                break;
2086
2087
            default:
2088
                throw new DomainException('Invalid list name: ' . $listname);
2089
        }
2090
2091
        $filters  = [];
2092
        $filters2 = [];
2093
        if (isset($attrs['filter1']) && count($this->list) > 0) {
2094
            foreach ($attrs as $key => $value) {
2095
                if (preg_match("/filter(\d)/", $key)) {
2096
                    $condition = $value;
2097
                    if (preg_match("/@(\w+)/", $condition, $match)) {
2098
                        $id    = $match[1];
2099
                        $value = "''";
2100
                        if ($id === 'ID') {
2101
                            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
2102
                                $value = "'" . $match[1] . "'";
2103
                            }
2104
                        } elseif ($id === 'fact') {
2105
                            $value = "'" . $this->fact . "'";
2106
                        } elseif ($id === 'desc') {
2107
                            $value = "'" . $this->desc . "'";
2108
                        } elseif (preg_match("/\d $id (.+)/", $this->gedrec, $match)) {
2109
                            $value = "'" . str_replace('@', '', trim($match[1])) . "'";
2110
                        }
2111
                        $condition = preg_replace("/@$id/", $value, $condition);
2112
                    }
2113
                    //-- handle regular expressions
2114
                    if (preg_match("/([A-Z:]+)\s*([^\s]+)\s*(.+)/", $condition, $match)) {
2115
                        $tag  = trim($match[1]);
2116
                        $expr = trim($match[2]);
2117
                        $val  = trim($match[3]);
2118
                        if (preg_match("/\\$(\w+)/", $val, $match)) {
2119
                            $val = $this->vars[$match[1]]['id'];
2120
                            $val = trim($val);
2121
                        }
2122
                        if ($val !== '') {
2123
                            $searchstr = '';
2124
                            $tags      = explode(':', $tag);
2125
                            //-- only limit to a level number if we are specifically looking at a level
2126
                            if (count($tags) > 1) {
2127
                                $level = 1;
2128
                                $t = 'XXXX';
2129
                                foreach ($tags as $t) {
2130
                                    if (!empty($searchstr)) {
2131
                                        $searchstr .= "[^\n]*(\n[2-9][^\n]*)*\n";
2132
                                    }
2133
                                    //-- search for both EMAIL and _EMAIL... silly double gedcom standard
2134
                                    if ($t === 'EMAIL' || $t === '_EMAIL') {
2135
                                        $t = '_?EMAIL';
2136
                                    }
2137
                                    $searchstr .= $level . ' ' . $t;
2138
                                    $level++;
2139
                                }
2140
                            } else {
2141
                                if ($tag === 'EMAIL' || $tag === '_EMAIL') {
2142
                                    $tag = '_?EMAIL';
2143
                                }
2144
                                $t         = $tag;
2145
                                $searchstr = '1 ' . $tag;
2146
                            }
2147
                            switch ($expr) {
2148
                                case 'CONTAINS':
2149
                                    if ($t === 'PLAC') {
2150
                                        $searchstr .= "[^\n]*[, ]*" . $val;
2151
                                    } else {
2152
                                        $searchstr .= "[^\n]*" . $val;
2153
                                    }
2154
                                    $filters[] = $searchstr;
2155
                                    break;
2156
                                default:
2157
                                    $filters2[] = [
2158
                                        'tag'  => $tag,
2159
                                        'expr' => $expr,
2160
                                        'val'  => $val,
2161
                                    ];
2162
                                    break;
2163
                            }
2164
                        }
2165
                    }
2166
                }
2167
            }
2168
        }
2169
        //-- apply other filters to the list that could not be added to the search string
2170
        if ($filters !== []) {
2171
            foreach ($this->list as $key => $record) {
2172
                foreach ($filters as $filter) {
2173
                    if (!preg_match('/' . $filter . '/i', $record->privatizeGedcom(Auth::accessLevel($this->tree)))) {
2174
                        unset($this->list[$key]);
2175
                        break;
2176
                    }
2177
                }
2178
            }
2179
        }
2180
        if ($filters2 !== []) {
2181
            $mylist = [];
2182
            foreach ($this->list as $indi) {
2183
                $key  = $indi->xref();
2184
                $grec = $indi->privatizeGedcom(Auth::accessLevel($this->tree));
2185
                $keep = true;
2186
                foreach ($filters2 as $filter) {
2187
                    if ($keep) {
2188
                        $tag  = $filter['tag'];
2189
                        $expr = $filter['expr'];
2190
                        $val  = $filter['val'];
2191
                        if ($val === "''") {
2192
                            $val = '';
2193
                        }
2194
                        $tags = explode(':', $tag);
2195
                        $t    = end($tags);
2196
                        $v    = $this->getGedcomValue($tag, 1, $grec);
2197
                        //-- check for EMAIL and _EMAIL (silly double gedcom standard :P)
2198
                        if ($t === 'EMAIL' && empty($v)) {
2199
                            $tag  = str_replace('EMAIL', '_EMAIL', $tag);
2200
                            $tags = explode(':', $tag);
2201
                            $t    = end($tags);
2202
                            $v    = self::getSubRecord(1, $tag, $grec);
2203
                        }
2204
2205
                        switch ($expr) {
2206
                            case 'GTE':
2207
                                if ($t === 'DATE') {
2208
                                    $date1 = new Date($v);
2209
                                    $date2 = new Date($val);
2210
                                    $keep  = (Date::compare($date1, $date2) >= 0);
2211
                                } elseif ($val >= $v) {
2212
                                    $keep = true;
2213
                                }
2214
                                break;
2215
                            case 'LTE':
2216
                                if ($t === 'DATE') {
2217
                                    $date1 = new Date($v);
2218
                                    $date2 = new Date($val);
2219
                                    $keep  = (Date::compare($date1, $date2) <= 0);
2220
                                } elseif ($val >= $v) {
2221
                                    $keep = true;
2222
                                }
2223
                                break;
2224
                            default:
2225
                                if ($v == $val) {
2226
                                    $keep = true;
2227
                                } else {
2228
                                    $keep = false;
2229
                                }
2230
                                break;
2231
                        }
2232
                    }
2233
                }
2234
                if ($keep) {
2235
                    $mylist[$key] = $indi;
2236
                }
2237
            }
2238
            $this->list = $mylist;
2239
        }
2240
2241
        switch ($sortby) {
2242
            case 'NAME':
2243
                uasort($this->list, GedcomRecord::nameComparator());
2244
                break;
2245
            case 'CHAN':
2246
                uasort($this->list, GedcomRecord::lastChangeComparator());
2247
                break;
2248
            case 'BIRT:DATE':
2249
                uasort($this->list, Individual::birthDateComparator());
2250
                break;
2251
            case 'DEAT:DATE':
2252
                uasort($this->list, Individual::deathDateComparator());
2253
                break;
2254
            case 'MARR:DATE':
2255
                uasort($this->list, Family::marriageDateComparator());
2256
                break;
2257
            default:
2258
                // unsorted or already sorted by SQL
2259
                break;
2260
        }
2261
2262
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
2263
        $this->repeat_bytes    = xml_get_current_line_number($this->parser) + 1;
2264
    }
2265
2266
    /**
2267
     * Handle </list>
2268
     *
2269
     * @return void
2270
     */
2271
    protected function listEndHandler(): void
2272
    {
2273
        $this->process_repeats--;
2274
        if ($this->process_repeats > 0) {
2275
            return;
2276
        }
2277
2278
        // Check if there is any list
2279
        if (count($this->list) > 0) {
2280
            $lineoffset = 0;
2281
            foreach ($this->repeats_stack as $rep) {
2282
                $lineoffset += $rep[1];
2283
            }
2284
            //-- read the xml from the file
2285
            $lines = file($this->report);
2286
            while ((!str_contains($lines[$lineoffset + $this->repeat_bytes], '<List')) && (($lineoffset + $this->repeat_bytes) > 0)) {
2287
                $lineoffset--;
2288
            }
2289
            $lineoffset++;
2290
            $reportxml = "<tempdoc>\n";
2291
            $line_nr   = $lineoffset + $this->repeat_bytes;
2292
            // List Level counter
2293
            $count = 1;
2294
            while (0 < $count) {
2295
                if (str_contains($lines[$line_nr], '<List')) {
2296
                    $count++;
2297
                } elseif (str_contains($lines[$line_nr], '</List')) {
2298
                    $count--;
2299
                }
2300
                if (0 < $count) {
2301
                    $reportxml .= $lines[$line_nr];
2302
                }
2303
                $line_nr++;
2304
            }
2305
            // No need to drag this
2306
            unset($lines);
2307
            $reportxml .= '</tempdoc>';
2308
            // Save original values
2309
            $this->parser_stack[] = $this->parser;
2310
            $oldgedrec            = $this->gedrec;
2311
2312
            $this->list_total   = count($this->list);
2313
            $this->list_private = 0;
2314
            foreach ($this->list as $record) {
2315
                if ($record->canShow()) {
2316
                    $this->gedrec = $record->privatizeGedcom(Auth::accessLevel($record->tree()));
2317
                    //-- start the sax parser
2318
                    $repeat_parser = xml_parser_create();
2319
                    $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...
2320
                    xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
2321
2322
                    xml_set_element_handler(
2323
                        $repeat_parser,
2324
                        function ($parser, string $name, array $attrs): void {
2325
                            $this->startElement($parser, $name, $attrs);
2326
                        },
2327
                        function ($parser, string $name): void {
2328
                            $this->endElement($parser, $name);
2329
                        }
2330
                    );
2331
2332
                    xml_set_character_data_handler(
2333
                        $repeat_parser,
2334
                        function ($parser, string $data): void {
2335
                            $this->characterData($parser, $data);
2336
                        }
2337
                    );
2338
2339
                    if (!xml_parse($repeat_parser, $reportxml, true)) {
2340
                        throw new DomainException(sprintf(
2341
                            'ListEHandler XML error: %s at line %d',
2342
                            xml_error_string(xml_get_error_code($repeat_parser)),
2343
                            xml_get_current_line_number($repeat_parser)
2344
                        ));
2345
                    }
2346
2347
                    if (PHP_MAJOR_VERSION < 8) {
2348
                        xml_parser_free($repeat_parser);
2349
                    }
2350
                } else {
2351
                    $this->list_private++;
2352
                }
2353
            }
2354
            $this->list   = [];
2355
            $this->parser = array_pop($this->parser_stack);
2356
            $this->gedrec = $oldgedrec;
2357
        }
2358
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
2359
    }
2360
2361
    /**
2362
     * Handle <listTotal>
2363
     * Prints the total number of records in a list
2364
     * The total number is collected from <list> and <relatives>
2365
     *
2366
     * @return void
2367
     */
2368
    protected function listTotalStartHandler(): void
2369
    {
2370
        if ($this->list_private == 0) {
2371
            $this->current_element->addText((string) $this->list_total);
2372
        } else {
2373
            $this->current_element->addText(($this->list_total - $this->list_private) . ' / ' . $this->list_total);
2374
        }
2375
    }
2376
2377
    /**
2378
     * Handle <relatives>
2379
     *
2380
     * @param array<string> $attrs
2381
     *
2382
     * @return void
2383
     */
2384
    protected function relativesStartHandler(array $attrs): void
2385
    {
2386
        $this->process_repeats++;
2387
        if ($this->process_repeats > 1) {
2388
            return;
2389
        }
2390
2391
        $sortby = $attrs['sortby'] ?? 'NAME';
2392
2393
        $match = [];
2394
        if (preg_match("/\\$(\w+)/", $sortby, $match)) {
2395
            $sortby = $this->vars[$match[1]]['id'];
2396
            $sortby = trim($sortby);
2397
        }
2398
2399
        $maxgen = -1;
2400
        if (isset($attrs['maxgen'])) {
2401
            $maxgen = (int) $attrs['maxgen'];
2402
        }
2403
2404
        $group = $attrs['group'] ?? 'child-family';
2405
2406
        if (preg_match("/\\$(\w+)/", $group, $match)) {
2407
            $group = $this->vars[$match[1]]['id'];
2408
            $group = trim($group);
2409
        }
2410
2411
        $id = $attrs['id'] ?? '';
2412
2413
        if (preg_match("/\\$(\w+)/", $id, $match)) {
2414
            $id = $this->vars[$match[1]]['id'];
2415
            $id = trim($id);
2416
        }
2417
2418
        $this->list = [];
2419
        $person     = Registry::individualFactory()->make($id, $this->tree);
2420
        if ($person instanceof Individual) {
2421
            $this->list[$id] = $person;
2422
            switch ($group) {
2423
                case 'child-family':
2424
                    foreach ($person->childFamilies() as $family) {
2425
                        foreach ($family->spouses() as $spouse) {
2426
                            $this->list[$spouse->xref()] = $spouse;
2427
                        }
2428
2429
                        foreach ($family->children() as $child) {
2430
                            $this->list[$child->xref()] = $child;
2431
                        }
2432
                    }
2433
                    break;
2434
                case 'spouse-family':
2435
                    foreach ($person->spouseFamilies() as $family) {
2436
                        foreach ($family->spouses() as $spouse) {
2437
                            $this->list[$spouse->xref()] = $spouse;
2438
                        }
2439
2440
                        foreach ($family->children() as $child) {
2441
                            $this->list[$child->xref()] = $child;
2442
                        }
2443
                    }
2444
                    break;
2445
                case 'direct-ancestors':
2446
                    $this->addAncestors($this->list, $id, false, $maxgen);
2447
                    break;
2448
                case 'ancestors':
2449
                    $this->addAncestors($this->list, $id, true, $maxgen);
2450
                    break;
2451
                case 'descendants':
2452
                    $this->list[$id]->generation = 1;
2453
                    $this->addDescendancy($this->list, $id, false, $maxgen);
2454
                    break;
2455
                case 'all':
2456
                    $this->addAncestors($this->list, $id, true, $maxgen);
2457
                    $this->addDescendancy($this->list, $id, true, $maxgen);
2458
                    break;
2459
            }
2460
        }
2461
2462
        switch ($sortby) {
2463
            case 'NAME':
2464
                uasort($this->list, GedcomRecord::nameComparator());
2465
                break;
2466
            case 'BIRT:DATE':
2467
                uasort($this->list, Individual::birthDateComparator());
2468
                break;
2469
            case 'DEAT:DATE':
2470
                uasort($this->list, Individual::deathDateComparator());
2471
                break;
2472
            case 'generation':
2473
                $newarray = [];
2474
                reset($this->list);
2475
                $genCounter = 1;
2476
                while (count($newarray) < count($this->list)) {
2477
                    foreach ($this->list as $key => $value) {
2478
                        $this->generation = $value->generation;
2479
                        if ($this->generation == $genCounter) {
2480
                            $newarray[$key] = (object) ['generation' => $this->generation];
2481
                        }
2482
                    }
2483
                    $genCounter++;
2484
                }
2485
                $this->list = $newarray;
2486
                break;
2487
            default:
2488
                // unsorted
2489
                break;
2490
        }
2491
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
2492
        $this->repeat_bytes    = xml_get_current_line_number($this->parser) + 1;
2493
    }
2494
2495
    /**
2496
     * Handle </relatives>
2497
     *
2498
     * @return void
2499
     */
2500
    protected function relativesEndHandler(): void
2501
    {
2502
        $this->process_repeats--;
2503
        if ($this->process_repeats > 0) {
2504
            return;
2505
        }
2506
2507
        // Check if there is any relatives
2508
        if (count($this->list) > 0) {
2509
            $lineoffset = 0;
2510
            foreach ($this->repeats_stack as $rep) {
2511
                $lineoffset += $rep[1];
2512
            }
2513
            //-- read the xml from the file
2514
            $lines = file($this->report);
2515
            while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<Relatives') && $lineoffset + $this->repeat_bytes > 0) {
2516
                $lineoffset--;
2517
            }
2518
            $lineoffset++;
2519
            $reportxml = "<tempdoc>\n";
2520
            $line_nr   = $lineoffset + $this->repeat_bytes;
2521
            // Relatives Level counter
2522
            $count = 1;
2523
            while (0 < $count) {
2524
                if (str_contains($lines[$line_nr], '<Relatives')) {
2525
                    $count++;
2526
                } elseif (str_contains($lines[$line_nr], '</Relatives')) {
2527
                    $count--;
2528
                }
2529
                if (0 < $count) {
2530
                    $reportxml .= $lines[$line_nr];
2531
                }
2532
                $line_nr++;
2533
            }
2534
            // No need to drag this
2535
            unset($lines);
2536
            $reportxml .= "</tempdoc>\n";
2537
            // Save original values
2538
            $this->parser_stack[] = $this->parser;
2539
            $oldgedrec            = $this->gedrec;
2540
2541
            $this->list_total   = count($this->list);
2542
            $this->list_private = 0;
2543
            foreach ($this->list as $xref => $value) {
2544
                if (isset($value->generation)) {
2545
                    $this->generation = $value->generation;
2546
                }
2547
                $tmp          = Registry::gedcomRecordFactory()->make((string) $xref, $this->tree);
2548
                $this->gedrec = $tmp->privatizeGedcom(Auth::accessLevel($this->tree));
2549
2550
                $repeat_parser = xml_parser_create();
2551
                $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...
2552
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, 0);
2553
2554
                xml_set_element_handler(
2555
                    $repeat_parser,
2556
                    function ($parser, string $name, array $attrs): void {
2557
                        $this->startElement($parser, $name, $attrs);
2558
                    },
2559
                    function ($parser, string $name): void {
2560
                        $this->endElement($parser, $name);
2561
                    }
2562
                );
2563
2564
                xml_set_character_data_handler(
2565
                    $repeat_parser,
2566
                    function ($parser, string $data): void {
2567
                        $this->characterData($parser, $data);
2568
                    }
2569
                );
2570
2571
                if (!xml_parse($repeat_parser, $reportxml, true)) {
2572
                    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)));
2573
                }
2574
2575
                if (PHP_MAJOR_VERSION < 8) {
2576
                    xml_parser_free($repeat_parser);
2577
                }
2578
            }
2579
            // Clean up the list array
2580
            $this->list   = [];
2581
            $this->parser = array_pop($this->parser_stack);
2582
            $this->gedrec = $oldgedrec;
2583
        }
2584
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
2585
    }
2586
2587
    /**
2588
     * Handle <generation />
2589
     * Prints the number of generations
2590
     *
2591
     * @return void
2592
     */
2593
    protected function generationStartHandler(): void
2594
    {
2595
        $this->current_element->addText((string) $this->generation);
2596
    }
2597
2598
    /**
2599
     * Handle <newPage />
2600
     * Has to be placed in an element (header, body or footer)
2601
     *
2602
     * @return void
2603
     */
2604
    protected function newPageStartHandler(): void
2605
    {
2606
        $temp = 'addpage';
2607
        $this->wt_report->addElement($temp);
2608
    }
2609
2610
    /**
2611
     * Handle </title>
2612
     *
2613
     * @return void
2614
     */
2615
    protected function titleEndHandler(): void
2616
    {
2617
        $this->report_root->addTitle($this->text);
2618
    }
2619
2620
    /**
2621
     * Handle </description>
2622
     *
2623
     * @return void
2624
     */
2625
    protected function descriptionEndHandler(): void
2626
    {
2627
        $this->report_root->addDescription($this->text);
2628
    }
2629
2630
    /**
2631
     * Create a list of all descendants.
2632
     *
2633
     * @param array<Individual> $list
2634
     * @param string            $pid
2635
     * @param bool              $parents
2636
     * @param int               $generations
2637
     *
2638
     * @return void
2639
     */
2640
    private function addDescendancy(&$list, $pid, $parents = false, $generations = -1): void
2641
    {
2642
        $person = Registry::individualFactory()->make($pid, $this->tree);
2643
        if ($person === null) {
2644
            return;
2645
        }
2646
        if (!isset($list[$pid])) {
2647
            $list[$pid] = $person;
2648
        }
2649
        if (!isset($list[$pid]->generation)) {
2650
            $list[$pid]->generation = 0;
2651
        }
2652
        foreach ($person->spouseFamilies() as $family) {
2653
            if ($parents) {
2654
                $husband = $family->husband();
2655
                $wife    = $family->wife();
2656
                if ($husband) {
2657
                    $list[$husband->xref()] = $husband;
2658
                    if (isset($list[$pid]->generation)) {
2659
                        $list[$husband->xref()]->generation = $list[$pid]->generation - 1;
2660
                    } else {
2661
                        $list[$husband->xref()]->generation = 1;
2662
                    }
2663
                }
2664
                if ($wife) {
2665
                    $list[$wife->xref()] = $wife;
2666
                    if (isset($list[$pid]->generation)) {
2667
                        $list[$wife->xref()]->generation = $list[$pid]->generation - 1;
2668
                    } else {
2669
                        $list[$wife->xref()]->generation = 1;
2670
                    }
2671
                }
2672
            }
2673
2674
            $children = $family->children();
2675
2676
            foreach ($children as $child) {
2677
                if ($child) {
2678
                    $list[$child->xref()] = $child;
2679
2680
                    if (isset($list[$pid]->generation)) {
2681
                        $list[$child->xref()]->generation = $list[$pid]->generation + 1;
2682
                    } else {
2683
                        $list[$child->xref()]->generation = 2;
2684
                    }
2685
                }
2686
            }
2687
            if ($generations == -1 || $list[$pid]->generation + 1 < $generations) {
2688
                foreach ($children as $child) {
2689
                    $this->addDescendancy($list, $child->xref(), $parents, $generations); // recurse on the childs family
2690
                }
2691
            }
2692
        }
2693
    }
2694
2695
    /**
2696
     * Create a list of all ancestors.
2697
     *
2698
     * @param array<Individual> $list
2699
     * @param string            $pid
2700
     * @param bool              $children
2701
     * @param int               $generations
2702
     *
2703
     * @return void
2704
     */
2705
    private function addAncestors(array &$list, string $pid, bool $children = false, int $generations = -1): void
2706
    {
2707
        $genlist                = [$pid];
2708
        $list[$pid]->generation = 1;
2709
        while (count($genlist) > 0) {
2710
            $id = array_shift($genlist);
2711
            if (str_starts_with($id, 'empty')) {
2712
                continue; // id can be something like “empty7”
2713
            }
2714
            $person = Registry::individualFactory()->make($id, $this->tree);
2715
            foreach ($person->childFamilies() as $family) {
2716
                $husband = $family->husband();
2717
                $wife    = $family->wife();
2718
                if ($husband) {
2719
                    $list[$husband->xref()]             = $husband;
2720
                    $list[$husband->xref()]->generation = $list[$id]->generation + 1;
2721
                }
2722
                if ($wife) {
2723
                    $list[$wife->xref()]             = $wife;
2724
                    $list[$wife->xref()]->generation = $list[$id]->generation + 1;
2725
                }
2726
                if ($generations == -1 || $list[$id]->generation + 1 < $generations) {
2727
                    if ($husband) {
2728
                        $genlist[] = $husband->xref();
2729
                    }
2730
                    if ($wife) {
2731
                        $genlist[] = $wife->xref();
2732
                    }
2733
                }
2734
                if ($children) {
2735
                    foreach ($family->children() as $child) {
2736
                        $list[$child->xref()] = $child;
2737
                        $list[$child->xref()]->generation = $list[$id]->generation ?? 1;
2738
                    }
2739
                }
2740
            }
2741
        }
2742
    }
2743
2744
    /**
2745
     * get gedcom tag value
2746
     *
2747
     * @param string $tag    The tag to find, use : to delineate subtags
2748
     * @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
2749
     * @param string $gedrec The gedcom record to get the value from
2750
     *
2751
     * @return string the value of a gedcom tag from the given gedcom record
2752
     */
2753
    private function getGedcomValue(string $tag, int $level, string $gedrec): string
2754
    {
2755
        if ($gedrec === '') {
2756
            return '';
2757
        }
2758
        $tags      = explode(':', $tag);
2759
        $origlevel = $level;
2760
        if ($level === 0) {
2761
            $level = 1 + (int) $gedrec[0];
2762
        }
2763
2764
        $subrec = $gedrec;
2765
        $t = 'XXXX';
2766
        foreach ($tags as $t) {
2767
            $lastsubrec = $subrec;
2768
            $subrec     = self::getSubRecord($level, "$level $t", $subrec);
2769
            if (empty($subrec) && $origlevel == 0) {
2770
                $level--;
2771
                $subrec = self::getSubRecord($level, "$level $t", $lastsubrec);
2772
            }
2773
            if (empty($subrec)) {
2774
                if ($t === 'TITL') {
2775
                    $subrec = self::getSubRecord($level, "$level ABBR", $lastsubrec);
2776
                    if (!empty($subrec)) {
2777
                        $t = 'ABBR';
2778
                    }
2779
                }
2780
                if ($subrec === '') {
2781
                    if ($level > 0) {
2782
                        $level--;
2783
                    }
2784
                    $subrec = self::getSubRecord($level, "@ $t", $gedrec);
2785
                    if ($subrec === '') {
2786
                        return '';
2787
                    }
2788
                }
2789
            }
2790
            $level++;
2791
        }
2792
        $level--;
2793
        $ct = preg_match("/$level $t(.*)/", $subrec, $match);
2794
        if ($ct === 0) {
2795
            $ct = preg_match("/$level @.+@ (.+)/", $subrec, $match);
2796
        }
2797
        if ($ct === 0) {
2798
            $ct = preg_match("/@ $t (.+)/", $subrec, $match);
2799
        }
2800
        if ($ct > 0) {
2801
            $value = trim($match[1]);
2802
            if ($t === 'NOTE' && preg_match('/^@(.+)@$/', $value, $match)) {
2803
                $note = Registry::noteFactory()->make($match[1], $this->tree);
2804
                if ($note instanceof Note) {
2805
                    $value = $note->getNote();
2806
                } else {
2807
                    //-- set the value to the id without the @
2808
                    $value = $match[1];
2809
                }
2810
            }
2811
            if ($level !== 0 || $t !== 'NOTE') {
2812
                $value .= self::getCont($level + 1, $subrec);
2813
            }
2814
2815
            if ($tag === 'NAME' || $tag === '_MARNM' || $tag === '_AKA') {
2816
                return strtr($value, ['/' => '']);
2817
            }
2818
2819
            return $value;
2820
        }
2821
2822
        return '';
2823
    }
2824
2825
    /**
2826
     * Replace variable identifiers with their values.
2827
     *
2828
     * @param string $expression An expression such as "$foo == 123"
2829
     * @param bool   $quote      Whether to add quotation marks
2830
     *
2831
     * @return string
2832
     */
2833
    private function substituteVars($expression, $quote): string
2834
    {
2835
        return preg_replace_callback(
2836
            '/\$(\w+)/',
2837
            function (array $matches) use ($quote): string {
2838
                if (isset($this->vars[$matches[1]]['id'])) {
2839
                    if ($quote) {
2840
                        return "'" . addcslashes($this->vars[$matches[1]]['id'], "'") . "'";
2841
                    }
2842
2843
                    return $this->vars[$matches[1]]['id'];
2844
                }
2845
2846
                Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1]));
2847
2848
                return '$' . $matches[1];
2849
            },
2850
            $expression
2851
        );
2852
    }
2853
}
2854