Passed
Push — dev ( 5f589d...824cd4 )
by Greg
08:12
created

ReportParserGenerate::gedcomEndHandler()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2021 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\Carbon;
25
use Fisharebest\Webtrees\Date;
26
use Fisharebest\Webtrees\Elements\UnknownElement;
27
use Fisharebest\Webtrees\Family;
28
use Fisharebest\Webtrees\Gedcom;
29
use Fisharebest\Webtrees\GedcomRecord;
30
use Fisharebest\Webtrees\I18N;
31
use Fisharebest\Webtrees\Individual;
32
use Fisharebest\Webtrees\Log;
33
use Fisharebest\Webtrees\MediaFile;
34
use Fisharebest\Webtrees\Note;
35
use Fisharebest\Webtrees\Place;
36
use Fisharebest\Webtrees\Registry;
37
use Fisharebest\Webtrees\Tree;
38
use Illuminate\Database\Capsule\Manager as DB;
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 call_user_func;
55
use function count;
56
use function end;
57
use function explode;
58
use function file;
59
use function file_exists;
60
use function getimagesize;
61
use function imagecreatefromstring;
62
use function imagesx;
63
use function imagesy;
64
use function in_array;
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_replace;
76
use function str_starts_with;
77
use function strip_tags;
78
use function strlen;
79
use function strtoupper;
80
use function substr;
81
use function trim;
82
use function uasort;
83
use function xml_error_string;
84
use function xml_get_current_line_number;
85
use function xml_get_error_code;
86
use function xml_parse;
87
use function xml_parser_create;
88
use function xml_parser_free;
89
use function xml_parser_set_option;
90
use function xml_set_character_data_handler;
91
use function xml_set_element_handler;
92
93
use const PREG_SET_ORDER;
94
use const XML_OPTION_CASE_FOLDING;
95
96
/**
97
 * Class ReportParserGenerate - parse a report.xml file and generate the report.
98
 */
99
class ReportParserGenerate extends ReportParserBase
100
{
101
    /** @var bool Are we collecting data from <Footnote> elements */
102
    private $process_footnote = true;
103
104
    /** @var bool Are we currently outputing data? */
105
    private $print_data = false;
106
107
    /** @var bool[] Push-down stack of $print_data */
108
    private $print_data_stack = [];
109
110
    /** @var int Are we processing GEDCOM data */
111
    private $process_gedcoms = 0;
112
113
    /** @var int Are we processing conditionals */
114
    private $process_ifs = 0;
115
116
    /** @var int Are we processing repeats */
117
    private $process_repeats = 0;
118
119
    /** @var int Quantity of data to repeat during loops */
120
    private $repeat_bytes = 0;
121
122
    /** @var array<string> Repeated data when iterating over loops */
123
    private array $repeats = [];
124
125
    /** @var array<int,array<int,array<string>|int>> Nested repeating data */
126
    private array $repeats_stack = [];
127
128
    /** @var array<AbstractRenderer> Nested repeating data */
129
    private array $wt_report_stack = [];
130
131
    /** @var XMLParser (resource before PHP 8.0) Nested repeating data */
132
    private $parser;
133
134
    /** @var XMLParser[] (resource[] before PHP 8.0) Nested repeating data */
135
    private array $parser_stack = [];
136
137
    /** @var string The current GEDCOM record */
138
    private $gedrec = '';
139
140
    /** @var array<string> Nested GEDCOM records */
141
    private array $gedrec_stack = [];
142
143
    /** @var ReportBaseElement The currently processed element */
144
    private $current_element;
145
146
    /** @var ReportBaseElement The currently processed element */
147
    private $footnote_element;
148
149
    /** @var string The GEDCOM fact currently being processed */
150
    private $fact = '';
151
152
    /** @var string The GEDCOM value currently being processed */
153
    private $desc = '';
154
155
    /** @var string The GEDCOM type currently being processed */
156
    private $type = '';
157
158
    /** @var int The current generational level */
159
    private $generation = 1;
160
161
    /** @var array Source data for processing lists */
162
    private array $list = [];
163
164
    /** @var int Number of items in lists */
165
    private $list_total = 0;
166
167
    /** @var int Number of items filtered from lists */
168
    private $list_private = 0;
169
170
    /** @var string The filename of the XML report */
171
    protected $report;
172
173
    /** @var AbstractRenderer A factory for creating report elements */
174
    private $report_root;
175
176
    /** @var AbstractRenderer Nested report elements */
177
    private $wt_report;
178
179
    /** @var array<array<string>> Variables defined in the report at run-time */
180
    private array $vars;
181
182
    private Tree $tree;
183
184
    private FilesystemOperator $data_filesystem;
185
186
    /**
187
     * Create a parser for a report
188
     *
189
     * @param string               $report The XML filename
190
     * @param AbstractRenderer     $report_root
191
     * @param array<array<string>> $vars
192
     * @param Tree                 $tree
193
     * @param FilesystemOperator   $data_filesystem
194
     */
195
    public function __construct(
196
        string $report,
197
        AbstractRenderer $report_root,
198
        array $vars,
199
        Tree $tree,
200
        FilesystemOperator $data_filesystem
201
    ) {
202
        $this->report          = $report;
203
        $this->report_root     = $report_root;
204
        $this->wt_report       = $report_root;
205
        $this->current_element = new ReportBaseElement();
206
        $this->vars            = $vars;
207
        $this->tree            = $tree;
208
        $this->data_filesystem = $data_filesystem;
209
210
        parent::__construct($report);
211
    }
212
213
    /**
214
     * get a gedcom subrecord
215
     *
216
     * searches a gedcom record and returns a subrecord of it. A subrecord is defined starting at a
217
     * line with level N and all subsequent lines greater than N until the next N level is reached.
218
     * For example, the following is a BIRT subrecord:
219
     * <code>1 BIRT
220
     * 2 DATE 1 JAN 1900
221
     * 2 PLAC Phoenix, Maricopa, Arizona</code>
222
     * The following example is the DATE subrecord of the above BIRT subrecord:
223
     * <code>2 DATE 1 JAN 1900</code>
224
     *
225
     * @param int    $level   the N level of the subrecord to get
226
     * @param string $tag     a gedcom tag or string to search for in the record (ie 1 BIRT or 2 DATE)
227
     * @param string $gedrec  the parent gedcom record to search in
228
     * @param int    $num     this allows you to specify which matching <var>$tag</var> to get. Oftentimes a
229
     *                        gedcom record will have more that 1 of the same type of subrecord. An individual may have
230
     *                        multiple events for example. Passing $num=1 would get the first 1. Passing $num=2 would get the
231
     *                        second one, etc.
232
     *
233
     * @return string the subrecord that was found or an empty string "" if not found.
234
     */
235
    public static function getSubRecord(int $level, string $tag, string $gedrec, int $num = 1): string
236
    {
237
        if ($gedrec === '') {
238
            return '';
239
        }
240
        // -- adding \n before and after gedrec
241
        $gedrec       = "\n" . $gedrec . "\n";
242
        $tag          = trim($tag);
243
        $searchTarget = "~[\n]" . $tag . "[\s]~";
244
        $ct           = preg_match_all($searchTarget, $gedrec, $match, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
245
        if ($ct === 0) {
246
            return '';
247
        }
248
        if ($ct < $num) {
249
            return '';
250
        }
251
        $pos1 = $match[$num - 1][0][1];
252
        $pos2 = strpos($gedrec, "\n$level", $pos1 + 1);
253
        if (!$pos2) {
254
            $pos2 = strpos($gedrec, "\n1", $pos1 + 1);
255
        }
256
        if (!$pos2) {
257
            $pos2 = strpos($gedrec, "\nWT_", $pos1 + 1); // WT_SPOUSE, WT_FAMILY_ID ...
258
        }
259
        if (!$pos2) {
260
            return ltrim(substr($gedrec, $pos1));
261
        }
262
        $subrec = substr($gedrec, $pos1, $pos2 - $pos1);
263
264
        return ltrim($subrec);
265
    }
266
267
    /**
268
     * get CONT lines
269
     *
270
     * get the N+1 CONT or CONC lines of a gedcom subrecord
271
     *
272
     * @param int    $nlevel the level of the CONT lines to get
273
     * @param string $nrec   the gedcom subrecord to search in
274
     *
275
     * @return string a string with all CONT lines merged
276
     */
277
    public static function getCont(int $nlevel, string $nrec): string
278
    {
279
        $text = '';
280
281
        $subrecords = explode("\n", $nrec);
282
        foreach ($subrecords as $thisSubrecord) {
283
            if (substr($thisSubrecord, 0, 2) !== $nlevel . ' ') {
284
                continue;
285
            }
286
            $subrecordType = substr($thisSubrecord, 2, 4);
287
            if ($subrecordType === 'CONT') {
288
                $text .= "\n" . substr($thisSubrecord, 7);
289
            }
290
        }
291
292
        return $text;
293
    }
294
295
    /**
296
     * XML start element handler
297
     * This function is called whenever a starting element is reached
298
     * The element handler will be called if found, otherwise it must be HTML
299
     *
300
     * @param resource      $parser the resource handler for the XML parser
301
     * @param string        $name   the name of the XML element parsed
302
     * @param array<string> $attrs  an array of key value pairs for the attributes
303
     *
304
     * @return void
305
     */
306
    protected function startElement($parser, string $name, array $attrs): void
307
    {
308
        $newattrs = [];
309
310
        foreach ($attrs as $key => $value) {
311
            if (preg_match("/^\\$(\w+)$/", $value, $match)) {
312
                if (isset($this->vars[$match[1]]['id']) && !isset($this->vars[$match[1]]['gedcom'])) {
313
                    $value = $this->vars[$match[1]]['id'];
314
                }
315
            }
316
            $newattrs[$key] = $value;
317
        }
318
        $attrs = $newattrs;
319
        if ($this->process_footnote && ($this->process_ifs === 0 || $name === 'if') && ($this->process_gedcoms === 0 || $name === 'Gedcom') && ($this->process_repeats === 0 || $name === 'Facts' || $name === 'RepeatTag')) {
320
            $method = $name . 'StartHandler';
321
322
            if (method_exists($this, $method)) {
323
                call_user_func([$this, $method], $attrs);
324
            }
325
        }
326
    }
327
328
    /**
329
     * XML end element handler
330
     * This function is called whenever an ending element is reached
331
     * The element handler will be called if found, otherwise it must be HTML
332
     *
333
     * @param resource $parser the resource handler for the XML parser
334
     * @param string   $name   the name of the XML element parsed
335
     *
336
     * @return void
337
     */
338
    protected function endElement($parser, string $name): void
339
    {
340
        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')) {
341
            $method = $name . 'EndHandler';
342
343
            if (method_exists($this, $method)) {
344
                call_user_func([$this, $method]);
345
            }
346
        }
347
    }
348
349
    /**
350
     * XML character data handler
351
     *
352
     * @param resource $parser the resource handler for the XML parser
353
     * @param string   $data   the name of the XML element parsed
354
     *
355
     * @return void
356
     */
357
    protected function characterData($parser, string $data): void
358
    {
359
        if ($this->print_data && $this->process_gedcoms === 0 && $this->process_ifs === 0 && $this->process_repeats === 0) {
360
            $this->current_element->addText($data);
361
        }
362
    }
363
364
    /**
365
     * Handle <style>
366
     *
367
     * @param array<string> $attrs
368
     *
369
     * @return void
370
     */
371
    protected function styleStartHandler(array $attrs): void
372
    {
373
        if (empty($attrs['name'])) {
374
            throw new DomainException('REPORT ERROR Style: The "name" of the style is missing or not set in the XML file.');
375
        }
376
377
        // array Style that will be passed on
378
        $s = [];
379
380
        // string Name af the style
381
        $s['name'] = $attrs['name'];
382
383
        // string Name of the DEFAULT font
384
        $s['font'] = $this->wt_report->default_font;
385
        if (!empty($attrs['font'])) {
386
            $s['font'] = $attrs['font'];
387
        }
388
389
        // int The size of the font in points
390
        $s['size'] = $this->wt_report->default_font_size;
391
        if (!empty($attrs['size'])) {
392
            // Get it as int to ignore all decimal points or text (if no text then 0)
393
            $s['size'] = (string) (int) $attrs['size'];
394
        }
395
396
        // string B: bold, I: italic, U: underline, D: line trough, The default value is regular.
397
        $s['style'] = '';
398
        if (!empty($attrs['style'])) {
399
            $s['style'] = $attrs['style'];
400
        }
401
402
        $this->wt_report->addStyle($s);
403
    }
404
405
    /**
406
     * Handle <doc>
407
     * Sets up the basics of the document proparties
408
     *
409
     * @param array<string> $attrs
410
     *
411
     * @return void
412
     */
413
    protected function docStartHandler(array $attrs): void
414
    {
415
        $this->parser = $this->xml_parser;
416
417
        // Custom page width
418
        if (!empty($attrs['customwidth'])) {
419
            $this->wt_report->page_width = (int) $attrs['customwidth'];
420
        } // Get it as int to ignore all decimal points or text (if any text then int(0))
421
        // Custom Page height
422
        if (!empty($attrs['customheight'])) {
423
            $this->wt_report->page_height = (int) $attrs['customheight'];
424
        } // Get it as int to ignore all decimal points or text (if any text then int(0))
425
426
        // Left Margin
427
        if (isset($attrs['leftmargin'])) {
428
            if ($attrs['leftmargin'] === '0') {
429
                $this->wt_report->left_margin = 0;
430
            } elseif (!empty($attrs['leftmargin'])) {
431
                $this->wt_report->left_margin = (int) $attrs['leftmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
432
            }
433
        }
434
        // Right Margin
435
        if (isset($attrs['rightmargin'])) {
436
            if ($attrs['rightmargin'] === '0') {
437
                $this->wt_report->right_margin = 0;
438
            } elseif (!empty($attrs['rightmargin'])) {
439
                $this->wt_report->right_margin = (int) $attrs['rightmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
440
            }
441
        }
442
        // Top Margin
443
        if (isset($attrs['topmargin'])) {
444
            if ($attrs['topmargin'] === '0') {
445
                $this->wt_report->top_margin = 0;
446
            } elseif (!empty($attrs['topmargin'])) {
447
                $this->wt_report->top_margin = (int) $attrs['topmargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
448
            }
449
        }
450
        // Bottom Margin
451
        if (isset($attrs['bottommargin'])) {
452
            if ($attrs['bottommargin'] === '0') {
453
                $this->wt_report->bottom_margin = 0;
454
            } elseif (!empty($attrs['bottommargin'])) {
455
                $this->wt_report->bottom_margin = (int) $attrs['bottommargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
456
            }
457
        }
458
        // Header Margin
459
        if (isset($attrs['headermargin'])) {
460
            if ($attrs['headermargin'] === '0') {
461
                $this->wt_report->header_margin = 0;
462
            } elseif (!empty($attrs['headermargin'])) {
463
                $this->wt_report->header_margin = (int) $attrs['headermargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
464
            }
465
        }
466
        // Footer Margin
467
        if (isset($attrs['footermargin'])) {
468
            if ($attrs['footermargin'] === '0') {
469
                $this->wt_report->footer_margin = 0;
470
            } elseif (!empty($attrs['footermargin'])) {
471
                $this->wt_report->footer_margin = (int) $attrs['footermargin']; // Get it as int to ignore all decimal points or text (if any text then int(0))
472
            }
473
        }
474
475
        // Page Orientation
476
        if (!empty($attrs['orientation'])) {
477
            if ($attrs['orientation'] === 'landscape') {
478
                $this->wt_report->orientation = 'landscape';
479
            } elseif ($attrs['orientation'] === 'portrait') {
480
                $this->wt_report->orientation = 'portrait';
481
            }
482
        }
483
        // Page Size
484
        if (!empty($attrs['pageSize'])) {
485
            $this->wt_report->page_format = strtoupper($attrs['pageSize']);
486
        }
487
488
        // Show Generated By...
489
        if (isset($attrs['showGeneratedBy'])) {
490
            if ($attrs['showGeneratedBy'] === '0') {
491
                $this->wt_report->show_generated_by = false;
492
            } elseif ($attrs['showGeneratedBy'] === '1') {
493
                $this->wt_report->show_generated_by = true;
494
            }
495
        }
496
497
        $this->wt_report->setup();
498
    }
499
500
    /**
501
     * Handle </doc>
502
     *
503
     * @return void
504
     */
505
    protected function docEndHandler(): void
506
    {
507
        $this->wt_report->run();
508
    }
509
510
    /**
511
     * Handle <header>
512
     *
513
     * @return void
514
     */
515
    protected function headerStartHandler(): void
516
    {
517
        // Clear the Header before any new elements are added
518
        $this->wt_report->clearHeader();
519
        $this->wt_report->setProcessing('H');
520
    }
521
522
    /**
523
     * Handle <body>
524
     *
525
     * @return void
526
     */
527
    protected function bodyStartHandler(): void
528
    {
529
        $this->wt_report->setProcessing('B');
530
    }
531
532
    /**
533
     * Handle <footer>
534
     *
535
     * @return void
536
     */
537
    protected function footerStartHandler(): void
538
    {
539
        $this->wt_report->setProcessing('F');
540
    }
541
542
    /**
543
     * Handle <cell>
544
     *
545
     * @param array<string,string> $attrs
546
     *
547
     * @return void
548
     */
549
    protected function cellStartHandler(array $attrs): void
550
    {
551
        // string The text alignment of the text in this box.
552
        $align = $attrs['align'] ?? '';
553
        // RTL supported left/right alignment
554
        if ($align === 'rightrtl') {
555
            if ($this->wt_report->rtl) {
556
                $align = 'left';
557
            } else {
558
                $align = 'right';
559
            }
560
        } elseif ($align === 'leftrtl') {
561
            if ($this->wt_report->rtl) {
562
                $align = 'right';
563
            } else {
564
                $align = 'left';
565
            }
566
        }
567
568
        // The color to fill the background of this cell
569
        $bgcolor = $attrs['bgcolor'] ?? '';
570
571
        // Whether the background should be painted
572
        $fill = (int) ($attrs['fill'] ?? '0');
573
574
        // If true reset the last cell height
575
        $reseth = (bool) ($attrs['reseth'] ?? '1');
576
577
        // Whether a border should be printed around this box
578
        $border = $attrs['border'] ?? '';
579
580
        // string Border color in HTML code
581
        $bocolor = $attrs['bocolor'] ?? '';
582
583
        // Cell height (expressed in points) The starting height of this cell. If the text wraps the height will automatically be adjusted.
584
        $height = (int) ($attrs['height'] ?? '0');
585
586
        // int Cell width (expressed in points) Setting the width to 0 will make it the width from the current location to the right margin.
587
        $width = (int) ($attrs['width'] ?? '0');
588
589
        // Stretch character mode
590
        $stretch = (int) ($attrs['stretch'] ?? '0');
591
592
        // mixed Position the left corner of this box on the page. The default is the current position.
593
        $left = ReportBaseElement::CURRENT_POSITION;
594
        if (isset($attrs['left'])) {
595
            if ($attrs['left'] === '.') {
596
                $left = ReportBaseElement::CURRENT_POSITION;
597
            } elseif (!empty($attrs['left'])) {
598
                $left = (int) $attrs['left'];
599
            } elseif ($attrs['left'] === '0') {
600
                $left = 0;
601
            }
602
        }
603
        // mixed Position the top corner of this box on the page. the default is the current position
604
        $top = ReportBaseElement::CURRENT_POSITION;
605
        if (isset($attrs['top'])) {
606
            if ($attrs['top'] === '.') {
607
                $top = ReportBaseElement::CURRENT_POSITION;
608
            } elseif (!empty($attrs['top'])) {
609
                $top = (int) $attrs['top'];
610
            } elseif ($attrs['top'] === '0') {
611
                $top = 0;
612
            }
613
        }
614
615
        // The name of the Style that should be used to render the text.
616
        $style = $attrs['style'] ?? '';
617
618
        // string Text color in html code
619
        $tcolor = $attrs['tcolor'] ?? '';
620
621
        // int Indicates where the current position should go after the call.
622
        $ln = 0;
623
        if (isset($attrs['newline'])) {
624
            if (!empty($attrs['newline'])) {
625
                $ln = (int) $attrs['newline'];
626
            } elseif ($attrs['newline'] === '0') {
627
                $ln = 0;
628
            }
629
        }
630
631
        if ($align === 'left') {
632
            $align = 'L';
633
        } elseif ($align === 'right') {
634
            $align = 'R';
635
        } elseif ($align === 'center') {
636
            $align = 'C';
637
        } elseif ($align === 'justify') {
638
            $align = 'J';
639
        }
640
641
        $this->print_data_stack[] = $this->print_data;
642
        $this->print_data         = true;
643
644
        $this->current_element = $this->report_root->createCell(
645
            (int) $width,
646
            (int) $height,
647
            $border,
648
            $align,
649
            $bgcolor,
650
            $style,
651
            $ln,
652
            $top,
653
            $left,
654
            $fill,
655
            $stretch,
656
            $bocolor,
657
            $tcolor,
658
            $reseth
659
        );
660
    }
661
662
    /**
663
     * Handle </cell>
664
     *
665
     * @return void
666
     */
667
    protected function cellEndHandler(): void
668
    {
669
        $this->print_data = array_pop($this->print_data_stack);
670
        $this->wt_report->addElement($this->current_element);
671
    }
672
673
    /**
674
     * Handle <now />
675
     *
676
     * @return void
677
     */
678
    protected function nowStartHandler(): void
679
    {
680
        $this->current_element->addText(Carbon::now()->local()->isoFormat('LLLL'));
681
    }
682
683
    /**
684
     * Handle <pageNum />
685
     *
686
     * @return void
687
     */
688
    protected function pageNumStartHandler(): void
689
    {
690
        $this->current_element->addText('#PAGENUM#');
691
    }
692
693
    /**
694
     * Handle <totalPages />
695
     *
696
     * @return void
697
     */
698
    protected function totalPagesStartHandler(): void
699
    {
700
        $this->current_element->addText('{{:ptp:}}');
701
    }
702
703
    /**
704
     * Called at the start of an element.
705
     *
706
     * @param array<string> $attrs an array of key value pairs for the attributes
707
     *
708
     * @return void
709
     */
710
    protected function gedcomStartHandler(array $attrs): void
711
    {
712
        if ($this->process_gedcoms > 0) {
713
            $this->process_gedcoms++;
714
715
            return;
716
        }
717
718
        $tag       = $attrs['id'];
719
        $tag       = str_replace('@fact', $this->fact, $tag);
720
        $tags      = explode(':', $tag);
721
        $newgedrec = '';
722
        if (count($tags) < 2) {
723
            $tmp       = Registry::gedcomRecordFactory()->make($attrs['id'], $this->tree);
724
            $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($this->tree)) : '';
725
        }
726
        if (empty($newgedrec)) {
727
            $tgedrec   = $this->gedrec;
728
            $newgedrec = '';
729
            foreach ($tags as $tag) {
730
                if (preg_match('/\$(.+)/', $tag, $match)) {
731
                    if (isset($this->vars[$match[1]]['gedcom'])) {
732
                        $newgedrec = $this->vars[$match[1]]['gedcom'];
733
                    } else {
734
                        $tmp       = Registry::gedcomRecordFactory()->make($match[1], $this->tree);
735
                        $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($this->tree)) : '';
736
                    }
737
                } else {
738
                    if (preg_match('/@(.+)/', $tag, $match)) {
739
                        $gmatch = [];
740
                        if (preg_match("/\d $match[1] @([^@]+)@/", $tgedrec, $gmatch)) {
741
                            $tmp       = Registry::gedcomRecordFactory()->make($gmatch[1], $this->tree);
742
                            $newgedrec = $tmp ? $tmp->privatizeGedcom(Auth::accessLevel($this->tree)) : '';
743
                            $tgedrec   = $newgedrec;
744
                        } else {
745
                            $newgedrec = '';
746
                            break;
747
                        }
748
                    } else {
749
                        $level     = 1 + (int) explode(' ', trim($tgedrec))[0];
750
                        $newgedrec = self::getSubRecord($level, "$level $tag", $tgedrec);
751
                        $tgedrec   = $newgedrec;
752
                    }
753
                }
754
            }
755
        }
756
        if (!empty($newgedrec)) {
757
            $this->gedrec_stack[] = [$this->gedrec, $this->fact, $this->desc];
758
            $this->gedrec         = $newgedrec;
759
            if (preg_match("/(\d+) (_?[A-Z0-9]+) (.*)/", $this->gedrec, $match)) {
760
                $this->fact = $match[2];
761
                $this->desc = trim($match[3]);
762
            }
763
        } else {
764
            $this->process_gedcoms++;
765
        }
766
    }
767
768
    /**
769
     * Called at the end of an element.
770
     *
771
     * @return void
772
     */
773
    protected function gedcomEndHandler(): void
774
    {
775
        if ($this->process_gedcoms > 0) {
776
            $this->process_gedcoms--;
777
        } else {
778
            [$this->gedrec, $this->fact, $this->desc] = array_pop($this->gedrec_stack);
779
        }
780
    }
781
782
    /**
783
     * Handle <textBox>
784
     *
785
     * @param array<string> $attrs
786
     *
787
     * @return void
788
     */
789
    protected function textBoxStartHandler(array $attrs): void
790
    {
791
        // string Background color code
792
        $bgcolor = '';
793
        if (!empty($attrs['bgcolor'])) {
794
            $bgcolor = $attrs['bgcolor'];
795
        }
796
797
        // boolean Wether or not fill the background color
798
        $fill = true;
799
        if (isset($attrs['fill'])) {
800
            if ($attrs['fill'] === '0') {
801
                $fill = false;
802
            } elseif ($attrs['fill'] === '1') {
803
                $fill = true;
804
            }
805
        }
806
807
        // var boolean Whether or not a border should be printed around this box. 0 = no border, 1 = border. Default is 0
808
        $border = false;
809
        if (isset($attrs['border'])) {
810
            if ($attrs['border'] === '1') {
811
                $border = true;
812
            } elseif ($attrs['border'] === '0') {
813
                $border = false;
814
            }
815
        }
816
817
        // int The starting height of this cell. If the text wraps the height will automatically be adjusted
818
        $height = 0;
819
        if (!empty($attrs['height'])) {
820
            $height = (int) $attrs['height'];
821
        }
822
        // int Setting the width to 0 will make it the width from the current location to the margin
823
        $width = 0;
824
        if (!empty($attrs['width'])) {
825
            $width = (int) $attrs['width'];
826
        }
827
828
        // mixed Position the left corner of this box on the page. The default is the current position.
829
        $left = ReportBaseElement::CURRENT_POSITION;
830
        if (isset($attrs['left'])) {
831
            if ($attrs['left'] === '.') {
832
                $left = ReportBaseElement::CURRENT_POSITION;
833
            } elseif (!empty($attrs['left'])) {
834
                $left = (int) $attrs['left'];
835
            } elseif ($attrs['left'] === '0') {
836
                $left = 0;
837
            }
838
        }
839
        // mixed Position the top corner of this box on the page. the default is the current position
840
        $top = ReportBaseElement::CURRENT_POSITION;
841
        if (isset($attrs['top'])) {
842
            if ($attrs['top'] === '.') {
843
                $top = ReportBaseElement::CURRENT_POSITION;
844
            } elseif (!empty($attrs['top'])) {
845
                $top = (int) $attrs['top'];
846
            } elseif ($attrs['top'] === '0') {
847
                $top = 0;
848
            }
849
        }
850
        // 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
851
        $newline = false;
852
        if (isset($attrs['newline'])) {
853
            if ($attrs['newline'] === '1') {
854
                $newline = true;
855
            } elseif ($attrs['newline'] === '0') {
856
                $newline = false;
857
            }
858
        }
859
        // boolean
860
        $pagecheck = true;
861
        if (isset($attrs['pagecheck'])) {
862
            if ($attrs['pagecheck'] === '0') {
863
                $pagecheck = false;
864
            } elseif ($attrs['pagecheck'] === '1') {
865
                $pagecheck = true;
866
            }
867
        }
868
        // boolean Cell padding
869
        $padding = true;
870
        if (isset($attrs['padding'])) {
871
            if ($attrs['padding'] === '0') {
872
                $padding = false;
873
            } elseif ($attrs['padding'] === '1') {
874
                $padding = true;
875
            }
876
        }
877
        // boolean Reset this box Height
878
        $reseth = false;
879
        if (isset($attrs['reseth'])) {
880
            if ($attrs['reseth'] === '1') {
881
                $reseth = true;
882
            } elseif ($attrs['reseth'] === '0') {
883
                $reseth = false;
884
            }
885
        }
886
887
        // string Style of rendering
888
        $style = '';
889
890
        $this->print_data_stack[] = $this->print_data;
891
        $this->print_data         = false;
892
893
        $this->wt_report_stack[] = $this->wt_report;
894
        $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...
895
            $width,
896
            $height,
897
            $border,
898
            $bgcolor,
899
            $newline,
900
            $left,
901
            $top,
902
            $pagecheck,
903
            $style,
904
            $fill,
905
            $padding,
906
            $reseth
907
        );
908
    }
909
910
    /**
911
     * Handle <textBox>
912
     *
913
     * @return void
914
     */
915
    protected function textBoxEndHandler(): void
916
    {
917
        $this->print_data      = array_pop($this->print_data_stack);
918
        $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...
919
920
        // The TextBox handler is mis-using the wt_report attribute to store an element.
921
        // Until this can be re-designed, we need this assertion to help static analysis tools.
922
        assert($this->current_element instanceof ReportBaseElement, new LogicException());
923
924
        $this->wt_report = array_pop($this->wt_report_stack);
925
        $this->wt_report->addElement($this->current_element);
926
    }
927
928
    /**
929
     * XLM <Text>.
930
     *
931
     * @param array<string> $attrs an array of key value pairs for the attributes
932
     *
933
     * @return void
934
     */
935
    protected function textStartHandler(array $attrs): void
936
    {
937
        $this->print_data_stack[] = $this->print_data;
938
        $this->print_data         = true;
939
940
        // string The name of the Style that should be used to render the text.
941
        $style = '';
942
        if (!empty($attrs['style'])) {
943
            $style = $attrs['style'];
944
        }
945
946
        // string  The color of the text - Keep the black color as default
947
        $color = '';
948
        if (!empty($attrs['color'])) {
949
            $color = $attrs['color'];
950
        }
951
952
        $this->current_element = $this->report_root->createText($style, $color);
953
    }
954
955
    /**
956
     * Handle </text>
957
     *
958
     * @return void
959
     */
960
    protected function textEndHandler(): void
961
    {
962
        $this->print_data = array_pop($this->print_data_stack);
963
        $this->wt_report->addElement($this->current_element);
964
    }
965
966
    /**
967
     * Handle <getPersonName />
968
     * Get the name
969
     * 1. id is empty - current GEDCOM record
970
     * 2. id is set with a record id
971
     *
972
     * @param array<string> $attrs an array of key value pairs for the attributes
973
     *
974
     * @return void
975
     */
976
    protected function getPersonNameStartHandler(array $attrs): void
977
    {
978
        $id    = '';
979
        $match = [];
980
        if (empty($attrs['id'])) {
981
            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
982
                $id = $match[1];
983
            }
984
        } else {
985
            if (preg_match('/\$(.+)/', $attrs['id'], $match)) {
986
                if (isset($this->vars[$match[1]]['id'])) {
987
                    $id = $this->vars[$match[1]]['id'];
988
                }
989
            } else {
990
                if (preg_match('/@(.+)/', $attrs['id'], $match)) {
991
                    $gmatch = [];
992
                    if (preg_match("/\d $match[1] @([^@]+)@/", $this->gedrec, $gmatch)) {
993
                        $id = $gmatch[1];
994
                    }
995
                } else {
996
                    $id = $attrs['id'];
997
                }
998
            }
999
        }
1000
        if (!empty($id)) {
1001
            $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1002
            if ($record === null) {
1003
                return;
1004
            }
1005
            if (!$record->canShowName()) {
1006
                $this->current_element->addText(I18N::translate('Private'));
1007
            } else {
1008
                $name = $record->fullName();
1009
                $name = strip_tags($name);
1010
                if (!empty($attrs['truncate'])) {
1011
                    $name = Str::limit($name, (int) $attrs['truncate'], I18N::translate('…'));
1012
                } else {
1013
                    $addname = (string) $record->alternateName();
1014
                    $addname = strip_tags($addname);
1015
                    if (!empty($addname)) {
1016
                        $name .= ' ' . $addname;
1017
                    }
1018
                }
1019
                $this->current_element->addText(trim($name));
1020
            }
1021
        }
1022
    }
1023
1024
    /**
1025
     * Handle <gedcomValue />
1026
     *
1027
     * @param array<string> $attrs
1028
     *
1029
     * @return void
1030
     */
1031
    protected function gedcomValueStartHandler(array $attrs): void
1032
    {
1033
        $id    = '';
1034
        $match = [];
1035
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1036
            $id = $match[1];
1037
        }
1038
1039
        if (isset($attrs['newline']) && $attrs['newline'] === '1') {
1040
            $useBreak = '1';
1041
        } else {
1042
            $useBreak = '0';
1043
        }
1044
1045
        $tag = $attrs['tag'];
1046
        if (!empty($tag)) {
1047
            if ($tag === '@desc') {
1048
                $value = $this->desc;
1049
                $value = trim($value);
1050
                $this->current_element->addText($value);
1051
            }
1052
            if ($tag === '@id') {
1053
                $this->current_element->addText($id);
1054
            } else {
1055
                $tag = str_replace('@fact', $this->fact, $tag);
1056
                if (empty($attrs['level'])) {
1057
                    $level = (int) explode(' ', trim($this->gedrec))[0];
1058
                    if ($level === 0) {
1059
                        $level++;
1060
                    }
1061
                } else {
1062
                    $level = (int) $attrs['level'];
1063
                }
1064
                $tags  = preg_split('/[: ]/', $tag);
1065
                $value = $this->getGedcomValue($tag, $level, $this->gedrec);
1066
                switch (end($tags)) {
1067
                    case 'DATE':
1068
                        $tmp   = new Date($value);
1069
                        $value = strip_tags($tmp->display());
1070
                        break;
1071
                    case 'PLAC':
1072
                        $tmp   = new Place($value, $this->tree);
1073
                        $value = $tmp->shortName();
1074
                        break;
1075
                }
1076
                if ($useBreak === '1') {
1077
                    // Insert <br> when multiple dates exist.
1078
                    // This works around a TCPDF bug that incorrectly wraps RTL dates on LTR pages
1079
                    $value = str_replace('(', '<br>(', $value);
1080
                    $value = str_replace('<span dir="ltr"><br>', '<br><span dir="ltr">', $value);
1081
                    $value = str_replace('<span dir="rtl"><br>', '<br><span dir="rtl">', $value);
1082
                    if (substr($value, 0, 4) === '<br>') {
1083
                        $value = substr($value, 4);
1084
                    }
1085
                }
1086
                $tmp = explode(':', $tag);
1087
                if (in_array(end($tmp), ['NOTE', 'TEXT'], true)) {
1088
                    if ($this->tree->getPreference('FORMAT_TEXT') === 'markdown') {
1089
                        $value = strip_tags(Registry::markdownFactory()->markdown($value, $this->tree));
1090
                    } else {
1091
                        $value = strip_tags(Registry::markdownFactory()->autolink($value, $this->tree));
1092
                    }
1093
                }
1094
1095
                if (!empty($attrs['truncate'])) {
1096
                    $value = Str::limit($value, (int) $attrs['truncate'], I18N::translate('…'));
1097
                }
1098
                $this->current_element->addText($value);
1099
            }
1100
        }
1101
    }
1102
1103
    /**
1104
     * Handle <repeatTag>
1105
     *
1106
     * @param array<string> $attrs
1107
     *
1108
     * @return void
1109
     */
1110
    protected function repeatTagStartHandler(array $attrs): void
1111
    {
1112
        $this->process_repeats++;
1113
        if ($this->process_repeats > 1) {
1114
            return;
1115
        }
1116
1117
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
1118
        $this->repeats         = [];
1119
        $this->repeat_bytes    = xml_get_current_line_number($this->parser);
1120
1121
        $tag = $attrs['tag'] ?? '';
1122
        if (!empty($tag)) {
1123
            if ($tag === '@desc') {
1124
                $value = $this->desc;
1125
                $value = trim($value);
1126
                $this->current_element->addText($value);
1127
            } else {
1128
                $tag   = str_replace('@fact', $this->fact, $tag);
1129
                $tags  = explode(':', $tag);
1130
                $level = (int) explode(' ', trim($this->gedrec))[0];
1131
                if ($level === 0) {
1132
                    $level++;
1133
                }
1134
                $subrec = $this->gedrec;
1135
                $t      = $tag;
1136
                $count  = count($tags);
1137
                $i      = 0;
1138
                while ($i < $count) {
1139
                    $t = $tags[$i];
1140
                    if (!empty($t)) {
1141
                        if ($i < ($count - 1)) {
1142
                            $subrec = self::getSubRecord($level, "$level $t", $subrec);
1143
                            if (empty($subrec)) {
1144
                                $level--;
1145
                                $subrec = self::getSubRecord($level, "@ $t", $this->gedrec);
1146
                                if (empty($subrec)) {
1147
                                    return;
1148
                                }
1149
                            }
1150
                        }
1151
                        $level++;
1152
                    }
1153
                    $i++;
1154
                }
1155
                $level--;
1156
                $count = preg_match_all("/$level $t(.*)/", $subrec, $match, PREG_SET_ORDER);
1157
                $i     = 0;
1158
                while ($i < $count) {
1159
                    $i++;
1160
                    // Privacy check - is this a link, and are we allowed to view the linked object?
1161
                    $subrecord = self::getSubRecord($level, "$level $t", $subrec, $i);
1162
                    if (preg_match('/^\d ' . Gedcom::REGEX_TAG . ' @(' . Gedcom::REGEX_XREF . ')@/', $subrecord, $xref_match)) {
1163
                        $linked_object = Registry::gedcomRecordFactory()->make($xref_match[1], $this->tree);
1164
                        if ($linked_object && !$linked_object->canShow()) {
1165
                            continue;
1166
                        }
1167
                    }
1168
                    $this->repeats[] = $subrecord;
1169
                }
1170
            }
1171
        }
1172
    }
1173
1174
    /**
1175
     * Handle </repeatTag>
1176
     *
1177
     * @return void
1178
     */
1179
    protected function repeatTagEndHandler(): void
1180
    {
1181
        $this->process_repeats--;
1182
        if ($this->process_repeats > 0) {
1183
            return;
1184
        }
1185
1186
        // Check if there is anything to repeat
1187
        if (count($this->repeats) > 0) {
1188
            // No need to load them if not used...
1189
1190
            $lineoffset = 0;
1191
            foreach ($this->repeats_stack as $rep) {
1192
                $lineoffset += $rep[1];
1193
            }
1194
            //-- read the xml from the file
1195
            $lines = file($this->report);
1196
            while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<RepeatTag')) {
1197
                $lineoffset--;
1198
            }
1199
            $lineoffset++;
1200
            $reportxml = "<tempdoc>\n";
1201
            $line_nr   = $lineoffset + $this->repeat_bytes;
1202
            // RepeatTag Level counter
1203
            $count = 1;
1204
            while (0 < $count) {
1205
                if (str_contains($lines[$line_nr], '<RepeatTag')) {
1206
                    $count++;
1207
                } elseif (str_contains($lines[$line_nr], '</RepeatTag')) {
1208
                    $count--;
1209
                }
1210
                if (0 < $count) {
1211
                    $reportxml .= $lines[$line_nr];
1212
                }
1213
                $line_nr++;
1214
            }
1215
            // No need to drag this
1216
            unset($lines);
1217
            $reportxml .= "</tempdoc>\n";
1218
            // Save original values
1219
            $this->parser_stack[] = $this->parser;
1220
            $oldgedrec            = $this->gedrec;
1221
            foreach ($this->repeats as $gedrec) {
1222
                $this->gedrec  = $gedrec;
1223
                $repeat_parser = xml_parser_create();
1224
                $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...
1225
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
1226
1227
                xml_set_element_handler(
1228
                    $repeat_parser,
1229
                    function ($parser, string $name, array $attrs): void {
1230
                        $this->startElement($parser, $name, $attrs);
1231
                    },
1232
                    function ($parser, string $name): void {
1233
                        $this->endElement($parser, $name);
1234
                    }
1235
                );
1236
1237
                xml_set_character_data_handler(
1238
                    $repeat_parser,
1239
                    function ($parser, string $data): void {
1240
                        $this->characterData($parser, $data);
1241
                    }
1242
                );
1243
1244
                if (!xml_parse($repeat_parser, $reportxml, true)) {
1245
                    throw new DomainException(sprintf(
1246
                        'RepeatTagEHandler XML error: %s at line %d',
1247
                        xml_error_string(xml_get_error_code($repeat_parser)),
1248
                        xml_get_current_line_number($repeat_parser)
1249
                    ));
1250
                }
1251
                xml_parser_free($repeat_parser);
1252
            }
1253
            // Restore original values
1254
            $this->gedrec = $oldgedrec;
1255
            $this->parser = array_pop($this->parser_stack);
1256
        }
1257
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
1258
    }
1259
1260
    /**
1261
     * Variable lookup
1262
     * Retrieve predefined variables :
1263
     * @ desc GEDCOM fact description, example:
1264
     *        1 EVEN This is a description
1265
     * @ fact GEDCOM fact tag, such as BIRT, DEAT etc.
1266
     * $ I18N::translate('....')
1267
     * $ language_settings[]
1268
     *
1269
     * @param array<string> $attrs an array of key value pairs for the attributes
1270
     *
1271
     * @return void
1272
     */
1273
    protected function varStartHandler(array $attrs): void
1274
    {
1275
        if (empty($attrs['var'])) {
1276
            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));
1277
        }
1278
1279
        $var = $attrs['var'];
1280
        // SetVar element preset variables
1281
        if (!empty($this->vars[$var]['id'])) {
1282
            $var = $this->vars[$var]['id'];
1283
        } else {
1284
            $tfact = $this->fact;
1285
            if (($this->fact === 'EVEN' || $this->fact === 'FACT') && $this->type !== '') {
1286
                // Use :
1287
                // n TYPE This text if string
1288
                $tfact = $this->type;
1289
            } else {
1290
                foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) {
1291
                    $element = Registry::elementFactory()->make($record_type . ':' . $this->fact);
1292
1293
                    if (!$element instanceof UnknownElement) {
1294
                        $tfact = $element->label();
1295
                        break;
1296
                    }
1297
                }
1298
            }
1299
1300
            $var = strtr($var, ['@desc' => $this->desc, '@fact' => $tfact]);
1301
1302
            if (preg_match('/^I18N::number\((.+)\)$/', $var, $match)) {
1303
                $var = I18N::number((int) $match[1]);
1304
            } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $var, $match)) {
1305
                $var = I18N::translate($match[1]);
1306
            } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $var, $match)) {
1307
                $var = I18N::translateContext($match[1], $match[2]);
1308
            }
1309
        }
1310
        // Check if variable is set as a date and reformat the date
1311
        if (isset($attrs['date'])) {
1312
            if ($attrs['date'] === '1') {
1313
                $g   = new Date($var);
1314
                $var = $g->display();
1315
            }
1316
        }
1317
        $this->current_element->addText($var);
1318
        $this->text = $var; // Used for title/descriptio
1319
    }
1320
1321
    /**
1322
     * Handle <facts>
1323
     *
1324
     * @param array<string> $attrs
1325
     *
1326
     * @return void
1327
     */
1328
    protected function factsStartHandler(array $attrs): void
1329
    {
1330
        $this->process_repeats++;
1331
        if ($this->process_repeats > 1) {
1332
            return;
1333
        }
1334
1335
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
1336
        $this->repeats         = [];
1337
        $this->repeat_bytes    = xml_get_current_line_number($this->parser);
1338
1339
        $id    = '';
1340
        $match = [];
1341
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1342
            $id = $match[1];
1343
        }
1344
        $tag = '';
1345
        if (isset($attrs['ignore'])) {
1346
            $tag .= $attrs['ignore'];
1347
        }
1348
        if (preg_match('/\$(.+)/', $tag, $match)) {
1349
            $tag = $this->vars[$match[1]]['id'];
1350
        }
1351
1352
        $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1353
        if (empty($attrs['diff']) && !empty($id)) {
1354
            $facts = $record->facts([], true);
1355
            $this->repeats = [];
1356
            $nonfacts      = explode(',', $tag);
1357
            foreach ($facts as $fact) {
1358
                $tag = explode(':', $fact->tag())[1];
1359
1360
                if (!in_array($tag, $nonfacts, true)) {
1361
                    $this->repeats[] = $fact->gedcom();
1362
                }
1363
            }
1364
        } else {
1365
            foreach ($record->facts() as $fact) {
1366
                if (($fact->isPendingAddition() || $fact->isPendingDeletion()) && !str_ends_with($fact->tag(), ':CHAN')) {
1367
                    $this->repeats[] = $fact->gedcom();
1368
                }
1369
            }
1370
        }
1371
    }
1372
1373
    /**
1374
     * Handle </facts>
1375
     *
1376
     * @return void
1377
     */
1378
    protected function factsEndHandler(): void
1379
    {
1380
        $this->process_repeats--;
1381
        if ($this->process_repeats > 0) {
1382
            return;
1383
        }
1384
1385
        // Check if there is anything to repeat
1386
        if (count($this->repeats) > 0) {
1387
            $line       = xml_get_current_line_number($this->parser) - 1;
1388
            $lineoffset = 0;
1389
            foreach ($this->repeats_stack as $rep) {
1390
                $lineoffset += $rep[1];
1391
            }
1392
1393
            //-- read the xml from the file
1394
            $lines = file($this->report);
1395
            while ($lineoffset + $this->repeat_bytes > 0 && !str_contains($lines[$lineoffset + $this->repeat_bytes], '<Facts ')) {
1396
                $lineoffset--;
1397
            }
1398
            $lineoffset++;
1399
            $reportxml = "<tempdoc>\n";
1400
            $i         = $line + $lineoffset;
1401
            $line_nr   = $this->repeat_bytes + $lineoffset;
1402
            while ($line_nr < $i) {
1403
                $reportxml .= $lines[$line_nr];
1404
                $line_nr++;
1405
            }
1406
            // No need to drag this
1407
            unset($lines);
1408
            $reportxml .= "</tempdoc>\n";
1409
            // Save original values
1410
            $this->parser_stack[] = $this->parser;
1411
            $oldgedrec            = $this->gedrec;
1412
            $count                = count($this->repeats);
1413
            $i                    = 0;
1414
            while ($i < $count) {
1415
                $this->gedrec = $this->repeats[$i];
1416
                $this->fact   = '';
1417
                $this->desc   = '';
1418
                if (preg_match('/1 (\w+)(.*)/', $this->gedrec, $match)) {
1419
                    $this->fact = $match[1];
1420
                    if ($this->fact === 'EVEN' || $this->fact === 'FACT') {
1421
                        $tmatch = [];
1422
                        if (preg_match('/2 TYPE (.+)/', $this->gedrec, $tmatch)) {
1423
                            $this->type = trim($tmatch[1]);
1424
                        } else {
1425
                            $this->type = ' ';
1426
                        }
1427
                    }
1428
                    $this->desc = trim($match[2]);
1429
                    $this->desc .= self::getCont(2, $this->gedrec);
1430
                }
1431
                $repeat_parser = xml_parser_create();
1432
                $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...
1433
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
1434
1435
                xml_set_element_handler(
1436
                    $repeat_parser,
1437
                    function ($parser, string $name, array $attrs): void {
1438
                        $this->startElement($parser, $name, $attrs);
1439
                    },
1440
                    function ($parser, string $name): void {
1441
                        $this->endElement($parser, $name);
1442
                    }
1443
                );
1444
1445
                xml_set_character_data_handler(
1446
                    $repeat_parser,
1447
                    function ($parser, string $data): void {
1448
                        $this->characterData($parser, $data);
1449
                    }
1450
                );
1451
1452
                if (!xml_parse($repeat_parser, $reportxml, true)) {
1453
                    throw new DomainException(sprintf(
1454
                        'FactsEHandler XML error: %s at line %d',
1455
                        xml_error_string(xml_get_error_code($repeat_parser)),
1456
                        xml_get_current_line_number($repeat_parser)
1457
                    ));
1458
                }
1459
                xml_parser_free($repeat_parser);
1460
                $i++;
1461
            }
1462
            // Restore original values
1463
            $this->parser = array_pop($this->parser_stack);
1464
            $this->gedrec = $oldgedrec;
1465
        }
1466
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
1467
    }
1468
1469
    /**
1470
     * Setting upp or changing variables in the XML
1471
     * The XML variable name and value is stored in $this->vars
1472
     *
1473
     * @param array<string> $attrs an array of key value pairs for the attributes
1474
     *
1475
     * @return void
1476
     */
1477
    protected function setVarStartHandler(array $attrs): void
1478
    {
1479
        if (empty($attrs['name'])) {
1480
            throw new DomainException('REPORT ERROR var: The attribute "name" is missing or not set in the XML file');
1481
        }
1482
1483
        $name  = $attrs['name'];
1484
        $value = $attrs['value'];
1485
        $match = [];
1486
        // Current GEDCOM record strings
1487
        if ($value === '@ID') {
1488
            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1489
                $value = $match[1];
1490
            }
1491
        } elseif ($value === '@fact') {
1492
            $value = $this->fact;
1493
        } elseif ($value === '@desc') {
1494
            $value = $this->desc;
1495
        } elseif ($value === '@generation') {
1496
            $value = (string) $this->generation;
1497
        } elseif (preg_match("/@(\w+)/", $value, $match)) {
1498
            $gmatch = [];
1499
            if (preg_match("/\d $match[1] (.+)/", $this->gedrec, $gmatch)) {
1500
                $value = str_replace('@', '', trim($gmatch[1]));
1501
            }
1502
        }
1503
        if (preg_match("/\\$(\w+)/", $name, $match)) {
1504
            $name = $this->vars["'" . $match[1] . "'"]['id'];
1505
        }
1506
        $count = preg_match_all("/\\$(\w+)/", $value, $match, PREG_SET_ORDER);
1507
        $i     = 0;
1508
        while ($i < $count) {
1509
            $t     = $this->vars[$match[$i][1]]['id'];
1510
            $value = preg_replace('/\$' . $match[$i][1] . '/', $t, $value, 1);
1511
            $i++;
1512
        }
1513
        if (preg_match('/^I18N::number\((.+)\)$/', $value, $match)) {
1514
            $value = I18N::number((int) $match[1]);
1515
        } elseif (preg_match('/^I18N::translate\(\'(.+)\'\)$/', $value, $match)) {
1516
            $value = I18N::translate($match[1]);
1517
        } elseif (preg_match('/^I18N::translateContext\(\'(.+)\', *\'(.+)\'\)$/', $value, $match)) {
1518
            $value = I18N::translateContext($match[1], $match[2]);
1519
        }
1520
1521
        // Arithmetic functions
1522
        if (preg_match("/(\d+)\s*([-+*\/])\s*(\d+)/", $value, $match)) {
1523
            // Create an expression language with the functions used by our reports.
1524
            $expression_provider  = new ReportExpressionLanguageProvider();
1525
            $expression_cache     = new NullAdapter();
1526
            $expression_language  = new ExpressionLanguage($expression_cache, [$expression_provider]);
1527
1528
            $value = (string) $expression_language->evaluate($value);
1529
        }
1530
1531
        if (str_contains($value, '@')) {
1532
            $value = '';
1533
        }
1534
        $this->vars[$name]['id'] = $value;
1535
    }
1536
1537
    /**
1538
     * Handle <if>
1539
     *
1540
     * @param array<string> $attrs
1541
     *
1542
     * @return void
1543
     */
1544
    protected function ifStartHandler(array $attrs): void
1545
    {
1546
        if ($this->process_ifs > 0) {
1547
            $this->process_ifs++;
1548
1549
            return;
1550
        }
1551
1552
        $condition = $attrs['condition'];
1553
        $condition = $this->substituteVars($condition, true);
1554
        $condition = str_replace([
1555
            ' LT ',
1556
            ' GT ',
1557
        ], [
1558
            '<',
1559
            '>',
1560
        ], $condition);
1561
        // Replace the first occurrence only once of @fact:DATE or in any other combinations to the current fact, such as BIRT
1562
        $condition = str_replace('@fact:', $this->fact . ':', $condition);
1563
        $match     = [];
1564
        $count     = preg_match_all("/@([\w:.]+)/", $condition, $match, PREG_SET_ORDER);
1565
        $i         = 0;
1566
        while ($i < $count) {
1567
            $id    = $match[$i][1];
1568
            $value = '""';
1569
            if ($id === 'ID') {
1570
                if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1571
                    $value = "'" . $match[1] . "'";
1572
                }
1573
            } elseif ($id === 'fact') {
1574
                $value = '"' . $this->fact . '"';
1575
            } elseif ($id === 'desc') {
1576
                $value = '"' . addslashes($this->desc) . '"';
1577
            } elseif ($id === 'generation') {
1578
                $value = '"' . $this->generation . '"';
1579
            } else {
1580
                $level = (int) explode(' ', trim($this->gedrec))[0];
1581
                if ($level === 0) {
1582
                    $level++;
1583
                }
1584
                $value = $this->getGedcomValue($id, $level, $this->gedrec);
1585
                if (empty($value)) {
1586
                    $level++;
1587
                    $value = $this->getGedcomValue($id, $level, $this->gedrec);
1588
                }
1589
                $value = preg_replace('/^@(' . Gedcom::REGEX_XREF . ')@$/', '$1', $value);
1590
                $value = '"' . addslashes($value) . '"';
1591
            }
1592
            $condition = str_replace("@$id", $value, $condition);
1593
            $i++;
1594
        }
1595
1596
        // Create an expression language with the functions used by our reports.
1597
        $expression_provider  = new ReportExpressionLanguageProvider();
1598
        $expression_cache     = new NullAdapter();
1599
        $expression_language  = new ExpressionLanguage($expression_cache, [$expression_provider]);
1600
1601
        $ret = $expression_language->evaluate($condition);
1602
1603
        if (!$ret) {
1604
            $this->process_ifs++;
1605
        }
1606
    }
1607
1608
    /**
1609
     * Handle </if>
1610
     *
1611
     * @return void
1612
     */
1613
    protected function ifEndHandler(): void
1614
    {
1615
        if ($this->process_ifs > 0) {
1616
            $this->process_ifs--;
1617
        }
1618
    }
1619
1620
    /**
1621
     * Handle <footnote>
1622
     * Collect the Footnote links
1623
     * GEDCOM Records that are protected by Privacy setting will be ignored
1624
     *
1625
     * @param array<string> $attrs
1626
     *
1627
     * @return void
1628
     */
1629
    protected function footnoteStartHandler(array $attrs): void
1630
    {
1631
        $id = '';
1632
        if (preg_match('/[0-9] (.+) @(.+)@/', $this->gedrec, $match)) {
1633
            $id = $match[2];
1634
        }
1635
        $record = Registry::gedcomRecordFactory()->make($id, $this->tree);
1636
        if ($record && $record->canShow()) {
1637
            $this->print_data_stack[] = $this->print_data;
1638
            $this->print_data         = true;
1639
            $style                    = '';
1640
            if (!empty($attrs['style'])) {
1641
                $style = $attrs['style'];
1642
            }
1643
            $this->footnote_element = $this->current_element;
1644
            $this->current_element  = $this->report_root->createFootnote($style);
1645
        } else {
1646
            $this->print_data       = false;
1647
            $this->process_footnote = false;
1648
        }
1649
    }
1650
1651
    /**
1652
     * Handle </footnote>
1653
     * Print the collected Footnote data
1654
     *
1655
     * @return void
1656
     */
1657
    protected function footnoteEndHandler(): void
1658
    {
1659
        if ($this->process_footnote) {
1660
            $this->print_data = array_pop($this->print_data_stack);
1661
            $temp             = trim($this->current_element->getValue());
1662
            if (strlen($temp) > 3) {
1663
                $this->wt_report->addElement($this->current_element);
1664
            }
1665
            $this->current_element = $this->footnote_element;
1666
        } else {
1667
            $this->process_footnote = true;
1668
        }
1669
    }
1670
1671
    /**
1672
     * Handle <footnoteTexts />
1673
     *
1674
     * @return void
1675
     */
1676
    protected function footnoteTextsStartHandler(): void
1677
    {
1678
        $temp = 'footnotetexts';
1679
        $this->wt_report->addElement($temp);
1680
    }
1681
1682
    /**
1683
     * XML element Forced line break handler - HTML code
1684
     *
1685
     * @return void
1686
     */
1687
    protected function brStartHandler(): void
1688
    {
1689
        if ($this->print_data && $this->process_gedcoms === 0) {
1690
            $this->current_element->addText('<br>');
1691
        }
1692
    }
1693
1694
    /**
1695
     * Handle <sp />
1696
     * Forced space
1697
     *
1698
     * @return void
1699
     */
1700
    protected function spStartHandler(): void
1701
    {
1702
        if ($this->print_data && $this->process_gedcoms === 0) {
1703
            $this->current_element->addText(' ');
1704
        }
1705
    }
1706
1707
    /**
1708
     * Handle <highlightedImage />
1709
     *
1710
     * @param array<string> $attrs
1711
     *
1712
     * @return void
1713
     */
1714
    protected function highlightedImageStartHandler(array $attrs): void
1715
    {
1716
        $id = '';
1717
        if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
1718
            $id = $match[1];
1719
        }
1720
1721
        // Position the top corner of this box on the page
1722
        $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION);
1723
1724
        // Position the left corner of this box on the page
1725
        $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION);
1726
1727
        // string Align the image in left, center, right (or empty to use x/y position).
1728
        $align = $attrs['align'] ?? '';
1729
1730
        // string Next Line should be T:next to the image, N:next line
1731
        $ln = $attrs['ln'] ?? 'T';
1732
1733
        // Width, height (or both).
1734
        $width  = (float) ($attrs['width'] ?? 0.0);
1735
        $height = (float) ($attrs['height'] ?? 0.0);
1736
1737
        $person     = Registry::individualFactory()->make($id, $this->tree);
1738
        $media_file = $person->findHighlightedMediaFile();
1739
1740
        if ($media_file instanceof MediaFile && $media_file->fileExists($this->data_filesystem)) {
1741
            $image      = imagecreatefromstring($media_file->fileContents($this->data_filesystem));
1742
            $attributes = [imagesx($image), imagesy($image)];
1743
1744
            if ($width > 0 && $height == 0) {
1745
                $perc   = $width / $attributes[0];
1746
                $height = round($attributes[1] * $perc);
1747
            } elseif ($height > 0 && $width == 0) {
1748
                $perc  = $height / $attributes[1];
1749
                $width = round($attributes[0] * $perc);
1750
            } else {
1751
                $width  = $attributes[0];
1752
                $height = $attributes[1];
1753
            }
1754
            $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln, $this->data_filesystem);
1755
            $this->wt_report->addElement($image);
1756
        }
1757
    }
1758
1759
    /**
1760
     * Handle <image/>
1761
     *
1762
     * @param array<string> $attrs
1763
     *
1764
     * @return void
1765
     */
1766
    protected function imageStartHandler(array $attrs): void
1767
    {
1768
        // Position the top corner of this box on the page. the default is the current position
1769
        $top = (float) ($attrs['top'] ?? ReportBaseElement::CURRENT_POSITION);
1770
1771
        // mixed Position the left corner of this box on the page. the default is the current position
1772
        $left = (float) ($attrs['left'] ?? ReportBaseElement::CURRENT_POSITION);
1773
1774
        // string Align the image in left, center, right (or empty to use x/y position).
1775
        $align = $attrs['align'] ?? '';
1776
1777
        // string Next Line should be T:next to the image, N:next line
1778
        $ln = $attrs['ln'] ?? 'T';
1779
1780
        // Width, height (or both).
1781
        $width  = (float) ($attrs['width'] ?? 0.0);
1782
        $height = (float) ($attrs['height'] ?? 0.0);
1783
1784
        $file = $attrs['file'] ?? '';
1785
1786
        if ($file === '@FILE') {
1787
            $match = [];
1788
            if (preg_match("/\d OBJE @(.+)@/", $this->gedrec, $match)) {
1789
                $mediaobject = Registry::mediaFactory()->make($match[1], $this->tree);
1790
                $media_file  = $mediaobject->firstImageFile();
1791
1792
                if ($media_file instanceof MediaFile && $media_file->fileExists($this->data_filesystem)) {
1793
                    $image      = imagecreatefromstring($media_file->fileContents($this->data_filesystem));
1794
                    $attributes = [imagesx($image), imagesy($image)];
1795
1796
                    if ($width > 0 && $height == 0) {
1797
                        $perc   = $width / $attributes[0];
1798
                        $height = round($attributes[1] * $perc);
1799
                    } elseif ($height > 0 && $width == 0) {
1800
                        $perc  = $height / $attributes[1];
1801
                        $width = round($attributes[0] * $perc);
1802
                    } else {
1803
                        $width  = $attributes[0];
1804
                        $height = $attributes[1];
1805
                    }
1806
                    $image = $this->report_root->createImageFromObject($media_file, $left, $top, $width, $height, $align, $ln, $this->data_filesystem);
1807
                    $this->wt_report->addElement($image);
1808
                }
1809
            }
1810
        } else {
1811
            if (file_exists($file) && preg_match('/(jpg|jpeg|png|gif)$/i', $file)) {
1812
                $size = getimagesize($file);
1813
                if ($width > 0 && $height == 0) {
1814
                    $perc   = $width / $size[0];
1815
                    $height = round($size[1] * $perc);
1816
                } elseif ($height > 0 && $width == 0) {
1817
                    $perc  = $height / $size[1];
1818
                    $width = round($size[0] * $perc);
1819
                } else {
1820
                    $width  = $size[0];
1821
                    $height = $size[1];
1822
                }
1823
                $image = $this->report_root->createImage($file, $left, $top, $width, $height, $align, $ln);
1824
                $this->wt_report->addElement($image);
1825
            }
1826
        }
1827
    }
1828
1829
    /**
1830
     * Handle <line>
1831
     *
1832
     * @param array<string> $attrs
1833
     *
1834
     * @return void
1835
     */
1836
    protected function lineStartHandler(array $attrs): void
1837
    {
1838
        // Start horizontal position, current position (default)
1839
        $x1 = ReportBaseElement::CURRENT_POSITION;
1840
        if (isset($attrs['x1'])) {
1841
            if ($attrs['x1'] === '0') {
1842
                $x1 = 0;
1843
            } elseif ($attrs['x1'] === '.') {
1844
                $x1 = ReportBaseElement::CURRENT_POSITION;
1845
            } elseif (!empty($attrs['x1'])) {
1846
                $x1 = (float) $attrs['x1'];
1847
            }
1848
        }
1849
        // Start vertical position, current position (default)
1850
        $y1 = ReportBaseElement::CURRENT_POSITION;
1851
        if (isset($attrs['y1'])) {
1852
            if ($attrs['y1'] === '0') {
1853
                $y1 = 0;
1854
            } elseif ($attrs['y1'] === '.') {
1855
                $y1 = ReportBaseElement::CURRENT_POSITION;
1856
            } elseif (!empty($attrs['y1'])) {
1857
                $y1 = (float) $attrs['y1'];
1858
            }
1859
        }
1860
        // End horizontal position, maximum width (default)
1861
        $x2 = ReportBaseElement::CURRENT_POSITION;
1862
        if (isset($attrs['x2'])) {
1863
            if ($attrs['x2'] === '0') {
1864
                $x2 = 0;
1865
            } elseif ($attrs['x2'] === '.') {
1866
                $x2 = ReportBaseElement::CURRENT_POSITION;
1867
            } elseif (!empty($attrs['x2'])) {
1868
                $x2 = (float) $attrs['x2'];
1869
            }
1870
        }
1871
        // End vertical position
1872
        $y2 = ReportBaseElement::CURRENT_POSITION;
1873
        if (isset($attrs['y2'])) {
1874
            if ($attrs['y2'] === '0') {
1875
                $y2 = 0;
1876
            } elseif ($attrs['y2'] === '.') {
1877
                $y2 = ReportBaseElement::CURRENT_POSITION;
1878
            } elseif (!empty($attrs['y2'])) {
1879
                $y2 = (float) $attrs['y2'];
1880
            }
1881
        }
1882
1883
        $line = $this->report_root->createLine($x1, $y1, $x2, $y2);
1884
        $this->wt_report->addElement($line);
1885
    }
1886
1887
    /**
1888
     * Handle <list>
1889
     *
1890
     * @param array<string> $attrs
1891
     *
1892
     * @return void
1893
     */
1894
    protected function listStartHandler(array $attrs): void
1895
    {
1896
        $this->process_repeats++;
1897
        if ($this->process_repeats > 1) {
1898
            return;
1899
        }
1900
1901
        $match = [];
1902
        if (isset($attrs['sortby'])) {
1903
            $sortby = $attrs['sortby'];
1904
            if (preg_match("/\\$(\w+)/", $sortby, $match)) {
1905
                $sortby = $this->vars[$match[1]]['id'];
1906
                $sortby = trim($sortby);
1907
            }
1908
        } else {
1909
            $sortby = 'NAME';
1910
        }
1911
1912
        $listname = $attrs['list'] ?? 'individual';
1913
1914
        // Some filters/sorts can be applied using SQL, while others require PHP
1915
        switch ($listname) {
1916
            case 'pending':
1917
                $this->list = DB::table('change')
1918
                    ->whereIn('change_id', function (Builder $query): void {
1919
                        $query->select(new Expression('MAX(change_id)'))
1920
                            ->from('change')
1921
                            ->where('gedcom_id', '=', $this->tree->id())
1922
                            ->where('status', '=', 'pending')
1923
                            ->groupBy(['xref']);
1924
                    })
1925
                    ->get()
1926
                    ->map(fn (object $row): ?GedcomRecord => Registry::gedcomRecordFactory()->make($row->xref, $this->tree, $row->new_gedcom ?: $row->old_gedcom))
1927
                    ->filter()
1928
                    ->all();
1929
                break;
1930
1931
            case 'individual':
1932
                $query = DB::table('individuals')
1933
                    ->where('i_file', '=', $this->tree->id())
1934
                    ->select(['i_id AS xref', 'i_gedcom AS gedcom'])
1935
                    ->distinct();
1936
1937
                foreach ($attrs as $attr => $value) {
1938
                    if (str_starts_with($attr, 'filter') && $value !== '') {
1939
                        $value = $this->substituteVars($value, false);
1940
                        // Convert the various filters into SQL
1941
                        if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
1942
                            $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void {
1943
                                $join
1944
                                    ->on($attr . '.d_gid', '=', 'i_id')
1945
                                    ->on($attr . '.d_file', '=', 'i_file');
1946
                            });
1947
1948
                            $query->where($attr . '.d_fact', '=', $match[1]);
1949
1950
                            $date = new Date($match[3]);
1951
1952
                            if ($match[2] === 'LTE') {
1953
                                $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay());
1954
                            } else {
1955
                                $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay());
1956
                            }
1957
1958
                            // This filter has been fully processed
1959
                            unset($attrs[$attr]);
1960
                        } elseif (preg_match('/^NAME CONTAINS (.+)$/', $value, $match)) {
1961
                            $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void {
1962
                                $join
1963
                                    ->on($attr . '.n_id', '=', 'i_id')
1964
                                    ->on($attr . '.n_file', '=', 'i_file');
1965
                            });
1966
                            // Search the DB only if there is any name supplied
1967
                            $names = explode(' ', $match[1]);
1968
                            foreach ($names as $n => $name) {
1969
                                $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%');
1970
                            }
1971
1972
                            // This filter has been fully processed
1973
                            unset($attrs[$attr]);
1974
                        } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) {
1975
                            // Convert newline escape sequences to actual new lines
1976
                            $match[1] = str_replace('\n', "\n", $match[1]);
1977
1978
                            $query->where('i_gedcom', 'LIKE', $match[1]);
1979
1980
                            // This filter has been fully processed
1981
                            unset($attrs[$attr]);
1982
                        } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) {
1983
                            // Don't unset this filter. This is just initial filtering for performance
1984
                            $query
1985
                                ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void {
1986
                                    $join
1987
                                        ->on($attr . 'a.pl_file', '=', 'i_file')
1988
                                        ->on($attr . 'a.pl_gid', '=', 'i_id');
1989
                                })
1990
                                ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void {
1991
                                    $join
1992
                                        ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file')
1993
                                        ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id');
1994
                                })
1995
                                ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%');
1996
                        } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) {
1997
                            // Don't unset this filter. This is just initial filtering for performance
1998
                            $match[3] = strtr($match[3], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
1999
                            $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%';
2000
                            $query->where('i_gedcom', 'LIKE', $like);
2001
                        } elseif (preg_match('/^(\w+) CONTAINS (.*)$/', $value, $match)) {
2002
                            // Don't unset this filter. This is just initial filtering for performance
2003
                            $match[2] = strtr($match[2], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2004
                            $like = "%\n1 " . $match[1] . '%' . $match[2] . '%';
2005
                            $query->where('i_gedcom', 'LIKE', $like);
2006
                        }
2007
                    }
2008
                }
2009
2010
                $this->list = [];
2011
2012
                foreach ($query->get() as $row) {
2013
                    $this->list[$row->xref] = Registry::individualFactory()->make($row->xref, $this->tree, $row->gedcom);
2014
                }
2015
                break;
2016
2017
            case 'family':
2018
                $query = DB::table('families')
2019
                    ->where('f_file', '=', $this->tree->id())
2020
                    ->select(['f_id AS xref', 'f_gedcom AS gedcom'])
2021
                    ->distinct();
2022
2023
                foreach ($attrs as $attr => $value) {
2024
                    if (str_starts_with($attr, 'filter') && $value !== '') {
2025
                        $value = $this->substituteVars($value, false);
2026
                        // Convert the various filters into SQL
2027
                        if (preg_match('/^(\w+):DATE (LTE|GTE) (.+)$/', $value, $match)) {
2028
                            $query->join('dates AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2029
                                $join
2030
                                    ->on($attr . '.d_gid', '=', 'f_id')
2031
                                    ->on($attr . '.d_file', '=', 'f_file');
2032
                            });
2033
2034
                            $query->where($attr . '.d_fact', '=', $match[1]);
2035
2036
                            $date = new Date($match[3]);
2037
2038
                            if ($match[2] === 'LTE') {
2039
                                $query->where($attr . '.d_julianday2', '<=', $date->maximumJulianDay());
2040
                            } else {
2041
                                $query->where($attr . '.d_julianday1', '>=', $date->minimumJulianDay());
2042
                            }
2043
2044
                            // This filter has been fully processed
2045
                            unset($attrs[$attr]);
2046
                        } elseif (preg_match('/^LIKE \/(.+)\/$/', $value, $match)) {
2047
                            // Convert newline escape sequences to actual new lines
2048
                            $match[1] = str_replace('\n', "\n", $match[1]);
2049
2050
                            $query->where('f_gedcom', 'LIKE', $match[1]);
2051
2052
                            // This filter has been fully processed
2053
                            unset($attrs[$attr]);
2054
                        } elseif (preg_match('/^NAME CONTAINS (.*)$/', $value, $match)) {
2055
                            if ($sortby === 'NAME' || $match[1] !== '') {
2056
                                $query->join('name AS ' . $attr, static function (JoinClause $join) use ($attr): void {
2057
                                    $join
2058
                                        ->on($attr . '.n_file', '=', 'f_file')
2059
                                        ->where(static function (Builder $query): void {
2060
                                            $query
2061
                                                ->whereColumn('n_id', '=', 'f_husb')
2062
                                                ->orWhereColumn('n_id', '=', 'f_wife');
2063
                                        });
2064
                                });
2065
                                // Search the DB only if there is any name supplied
2066
                                if ($match[1] != '') {
2067
                                    $names = explode(' ', $match[1]);
2068
                                    foreach ($names as $n => $name) {
2069
                                        $query->where($attr . '.n_full', 'LIKE', '%' . addcslashes($name, '\\%_') . '%');
2070
                                    }
2071
                                }
2072
                            }
2073
2074
                            // This filter has been fully processed
2075
                            unset($attrs[$attr]);
2076
                        } elseif (preg_match('/^(?:\w*):PLAC CONTAINS (.+)$/', $value, $match)) {
2077
                            // Don't unset this filter. This is just initial filtering for performance
2078
                            $query
2079
                                ->join('placelinks AS ' . $attr . 'a', static function (JoinClause $join) use ($attr): void {
2080
                                    $join
2081
                                        ->on($attr . 'a.pl_file', '=', 'f_file')
2082
                                        ->on($attr . 'a.pl_gid', '=', 'f_id');
2083
                                })
2084
                                ->join('places AS ' . $attr . 'b', static function (JoinClause $join) use ($attr): void {
2085
                                    $join
2086
                                        ->on($attr . 'b.p_file', '=', $attr . 'a.pl_file')
2087
                                        ->on($attr . 'b.p_id', '=', $attr . 'a.pl_p_id');
2088
                                })
2089
                                ->where($attr . 'b.p_place', 'LIKE', '%' . addcslashes($match[1], '\\%_') . '%');
2090
                        } elseif (preg_match('/^(\w*):(\w+) CONTAINS (.+)$/', $value, $match)) {
2091
                            // Don't unset this filter. This is just initial filtering for performance
2092
                            $match[3] = strtr($match[3], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2093
                            $like = "%\n1 " . $match[1] . "%\n2 " . $match[2] . '%' . $match[3] . '%';
2094
                            $query->where('f_gedcom', 'LIKE', $like);
2095
                        } elseif (preg_match('/^(\w+) CONTAINS (.+)$/', $value, $match)) {
2096
                            // Don't unset this filter. This is just initial filtering for performance
2097
                            $match[2] = strtr($match[2], ['\\' => '\\\\', '%'  => '\\%', '_'  => '\\_', ' ' => '%']);
2098
                            $like = "%\n1 " . $match[1] . '%' . $match[2] . '%';
2099
                            $query->where('f_gedcom', 'LIKE', $like);
2100
                        }
2101
                    }
2102
                }
2103
2104
                $this->list = [];
2105
2106
                foreach ($query->get() as $row) {
2107
                    $this->list[$row->xref] = Registry::familyFactory()->make($row->xref, $this->tree, $row->gedcom);
2108
                }
2109
                break;
2110
2111
            default:
2112
                throw new DomainException('Invalid list name: ' . $listname);
2113
        }
2114
2115
        $filters  = [];
2116
        $filters2 = [];
2117
        if (isset($attrs['filter1']) && count($this->list) > 0) {
2118
            foreach ($attrs as $key => $value) {
2119
                if (preg_match("/filter(\d)/", $key)) {
2120
                    $condition = $value;
2121
                    if (preg_match("/@(\w+)/", $condition, $match)) {
2122
                        $id    = $match[1];
2123
                        $value = "''";
2124
                        if ($id === 'ID') {
2125
                            if (preg_match('/0 @(.+)@/', $this->gedrec, $match)) {
2126
                                $value = "'" . $match[1] . "'";
2127
                            }
2128
                        } elseif ($id === 'fact') {
2129
                            $value = "'" . $this->fact . "'";
2130
                        } elseif ($id === 'desc') {
2131
                            $value = "'" . $this->desc . "'";
2132
                        } else {
2133
                            if (preg_match("/\d $id (.+)/", $this->gedrec, $match)) {
2134
                                $value = "'" . str_replace('@', '', trim($match[1])) . "'";
2135
                            }
2136
                        }
2137
                        $condition = preg_replace("/@$id/", $value, $condition);
2138
                    }
2139
                    //-- handle regular expressions
2140
                    if (preg_match("/([A-Z:]+)\s*([^\s]+)\s*(.+)/", $condition, $match)) {
2141
                        $tag  = trim($match[1]);
2142
                        $expr = trim($match[2]);
2143
                        $val  = trim($match[3]);
2144
                        if (preg_match("/\\$(\w+)/", $val, $match)) {
2145
                            $val = $this->vars[$match[1]]['id'];
2146
                            $val = trim($val);
2147
                        }
2148
                        if ($val) {
2149
                            $searchstr = '';
2150
                            $tags      = explode(':', $tag);
2151
                            //-- only limit to a level number if we are specifically looking at a level
2152
                            if (count($tags) > 1) {
2153
                                $level = 1;
2154
                                $t = 'XXXX';
2155
                                foreach ($tags as $t) {
2156
                                    if (!empty($searchstr)) {
2157
                                        $searchstr .= "[^\n]*(\n[2-9][^\n]*)*\n";
2158
                                    }
2159
                                    //-- search for both EMAIL and _EMAIL... silly double gedcom standard
2160
                                    if ($t === 'EMAIL' || $t === '_EMAIL') {
2161
                                        $t = '_?EMAIL';
2162
                                    }
2163
                                    $searchstr .= $level . ' ' . $t;
2164
                                    $level++;
2165
                                }
2166
                            } else {
2167
                                if ($tag === 'EMAIL' || $tag === '_EMAIL') {
2168
                                    $tag = '_?EMAIL';
2169
                                }
2170
                                $t         = $tag;
2171
                                $searchstr = '1 ' . $tag;
2172
                            }
2173
                            switch ($expr) {
2174
                                case 'CONTAINS':
2175
                                    if ($t === 'PLAC') {
2176
                                        $searchstr .= "[^\n]*[, ]*" . $val;
2177
                                    } else {
2178
                                        $searchstr .= "[^\n]*" . $val;
2179
                                    }
2180
                                    $filters[] = $searchstr;
2181
                                    break;
2182
                                default:
2183
                                    $filters2[] = [
2184
                                        'tag'  => $tag,
2185
                                        'expr' => $expr,
2186
                                        'val'  => $val,
2187
                                    ];
2188
                                    break;
2189
                            }
2190
                        }
2191
                    }
2192
                }
2193
            }
2194
        }
2195
        //-- apply other filters to the list that could not be added to the search string
2196
        if ($filters) {
2197
            foreach ($this->list as $key => $record) {
2198
                foreach ($filters as $filter) {
2199
                    if (!preg_match('/' . $filter . '/i', $record->privatizeGedcom(Auth::accessLevel($this->tree)))) {
2200
                        unset($this->list[$key]);
2201
                        break;
2202
                    }
2203
                }
2204
            }
2205
        }
2206
        if ($filters2) {
2207
            $mylist = [];
2208
            foreach ($this->list as $indi) {
2209
                $key  = $indi->xref();
2210
                $grec = $indi->privatizeGedcom(Auth::accessLevel($this->tree));
2211
                $keep = true;
2212
                foreach ($filters2 as $filter) {
2213
                    if ($keep) {
2214
                        $tag  = $filter['tag'];
2215
                        $expr = $filter['expr'];
2216
                        $val  = $filter['val'];
2217
                        if ($val === "''") {
2218
                            $val = '';
2219
                        }
2220
                        $tags = explode(':', $tag);
2221
                        $t    = end($tags);
2222
                        $v    = $this->getGedcomValue($tag, 1, $grec);
2223
                        //-- check for EMAIL and _EMAIL (silly double gedcom standard :P)
2224
                        if ($t === 'EMAIL' && empty($v)) {
2225
                            $tag  = str_replace('EMAIL', '_EMAIL', $tag);
2226
                            $tags = explode(':', $tag);
2227
                            $t    = end($tags);
2228
                            $v    = self::getSubRecord(1, $tag, $grec);
2229
                        }
2230
2231
                        switch ($expr) {
2232
                            case 'GTE':
2233
                                if ($t === 'DATE') {
2234
                                    $date1 = new Date($v);
2235
                                    $date2 = new Date($val);
2236
                                    $keep  = (Date::compare($date1, $date2) >= 0);
2237
                                } elseif ($val >= $v) {
2238
                                    $keep = true;
2239
                                }
2240
                                break;
2241
                            case 'LTE':
2242
                                if ($t === 'DATE') {
2243
                                    $date1 = new Date($v);
2244
                                    $date2 = new Date($val);
2245
                                    $keep  = (Date::compare($date1, $date2) <= 0);
2246
                                } elseif ($val >= $v) {
2247
                                    $keep = true;
2248
                                }
2249
                                break;
2250
                            default:
2251
                                if ($v == $val) {
2252
                                    $keep = true;
2253
                                } else {
2254
                                    $keep = false;
2255
                                }
2256
                                break;
2257
                        }
2258
                    }
2259
                }
2260
                if ($keep) {
2261
                    $mylist[$key] = $indi;
2262
                }
2263
            }
2264
            $this->list = $mylist;
2265
        }
2266
2267
        switch ($sortby) {
2268
            case 'NAME':
2269
                uasort($this->list, GedcomRecord::nameComparator());
2270
                break;
2271
            case 'CHAN':
2272
                uasort($this->list, GedcomRecord::lastChangeComparator());
2273
                break;
2274
            case 'BIRT:DATE':
2275
                uasort($this->list, Individual::birthDateComparator());
2276
                break;
2277
            case 'DEAT:DATE':
2278
                uasort($this->list, Individual::deathDateComparator());
2279
                break;
2280
            case 'MARR:DATE':
2281
                uasort($this->list, Family::marriageDateComparator());
2282
                break;
2283
            default:
2284
                // unsorted or already sorted by SQL
2285
                break;
2286
        }
2287
2288
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
2289
        $this->repeat_bytes    = xml_get_current_line_number($this->parser) + 1;
2290
    }
2291
2292
    /**
2293
     * Handle </list>
2294
     *
2295
     * @return void
2296
     */
2297
    protected function listEndHandler(): void
2298
    {
2299
        $this->process_repeats--;
2300
        if ($this->process_repeats > 0) {
2301
            return;
2302
        }
2303
2304
        // Check if there is any list
2305
        if (count($this->list) > 0) {
2306
            $lineoffset = 0;
2307
            foreach ($this->repeats_stack as $rep) {
2308
                $lineoffset += $rep[1];
2309
            }
2310
            //-- read the xml from the file
2311
            $lines = file($this->report);
2312
            while ((!str_contains($lines[$lineoffset + $this->repeat_bytes], '<List')) && (($lineoffset + $this->repeat_bytes) > 0)) {
2313
                $lineoffset--;
2314
            }
2315
            $lineoffset++;
2316
            $reportxml = "<tempdoc>\n";
2317
            $line_nr   = $lineoffset + $this->repeat_bytes;
2318
            // List Level counter
2319
            $count = 1;
2320
            while (0 < $count) {
2321
                if (str_contains($lines[$line_nr], '<List')) {
2322
                    $count++;
2323
                } elseif (str_contains($lines[$line_nr], '</List')) {
2324
                    $count--;
2325
                }
2326
                if (0 < $count) {
2327
                    $reportxml .= $lines[$line_nr];
2328
                }
2329
                $line_nr++;
2330
            }
2331
            // No need to drag this
2332
            unset($lines);
2333
            $reportxml .= '</tempdoc>';
2334
            // Save original values
2335
            $this->parser_stack[] = $this->parser;
2336
            $oldgedrec            = $this->gedrec;
2337
2338
            $this->list_total   = count($this->list);
2339
            $this->list_private = 0;
2340
            foreach ($this->list as $record) {
2341
                if ($record->canShow()) {
2342
                    $this->gedrec = $record->privatizeGedcom(Auth::accessLevel($record->tree()));
2343
                    //-- start the sax parser
2344
                    $repeat_parser = xml_parser_create();
2345
                    $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...
2346
                    xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
2347
2348
                    xml_set_element_handler(
2349
                        $repeat_parser,
2350
                        function ($parser, string $name, array $attrs): void {
2351
                            $this->startElement($parser, $name, $attrs);
2352
                        },
2353
                        function ($parser, string $name): void {
2354
                            $this->endElement($parser, $name);
2355
                        }
2356
                    );
2357
2358
                    xml_set_character_data_handler(
2359
                        $repeat_parser,
2360
                        function ($parser, string $data): void {
2361
                            $this->characterData($parser, $data);
2362
                        }
2363
                    );
2364
2365
                    if (!xml_parse($repeat_parser, $reportxml, true)) {
2366
                        throw new DomainException(sprintf(
2367
                            'ListEHandler XML error: %s at line %d',
2368
                            xml_error_string(xml_get_error_code($repeat_parser)),
2369
                            xml_get_current_line_number($repeat_parser)
2370
                        ));
2371
                    }
2372
                    xml_parser_free($repeat_parser);
2373
                } else {
2374
                    $this->list_private++;
2375
                }
2376
            }
2377
            $this->list   = [];
2378
            $this->parser = array_pop($this->parser_stack);
2379
            $this->gedrec = $oldgedrec;
2380
        }
2381
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
2382
    }
2383
2384
    /**
2385
     * Handle <listTotal>
2386
     * Prints the total number of records in a list
2387
     * The total number is collected from <list> and <relatives>
2388
     *
2389
     * @return void
2390
     */
2391
    protected function listTotalStartHandler(): void
2392
    {
2393
        if ($this->list_private == 0) {
2394
            $this->current_element->addText((string) $this->list_total);
2395
        } else {
2396
            $this->current_element->addText(($this->list_total - $this->list_private) . ' / ' . $this->list_total);
2397
        }
2398
    }
2399
2400
    /**
2401
     * Handle <relatives>
2402
     *
2403
     * @param array<string> $attrs
2404
     *
2405
     * @return void
2406
     */
2407
    protected function relativesStartHandler(array $attrs): void
2408
    {
2409
        $this->process_repeats++;
2410
        if ($this->process_repeats > 1) {
2411
            return;
2412
        }
2413
2414
        $sortby = $attrs['sortby'] ?? 'NAME';
2415
2416
        $match = [];
2417
        if (preg_match("/\\$(\w+)/", $sortby, $match)) {
2418
            $sortby = $this->vars[$match[1]]['id'];
2419
            $sortby = trim($sortby);
2420
        }
2421
2422
        $maxgen = -1;
2423
        if (isset($attrs['maxgen'])) {
2424
            $maxgen = (int) $attrs['maxgen'];
2425
        }
2426
2427
        $group = $attrs['group'] ?? 'child-family';
2428
2429
        if (preg_match("/\\$(\w+)/", $group, $match)) {
2430
            $group = $this->vars[$match[1]]['id'];
2431
            $group = trim($group);
2432
        }
2433
2434
        $id = $attrs['id'] ?? '';
2435
2436
        if (preg_match("/\\$(\w+)/", $id, $match)) {
2437
            $id = $this->vars[$match[1]]['id'];
2438
            $id = trim($id);
2439
        }
2440
2441
        $this->list = [];
2442
        $person     = Registry::individualFactory()->make($id, $this->tree);
2443
        if ($person instanceof Individual) {
2444
            $this->list[$id] = $person;
2445
            switch ($group) {
2446
                case 'child-family':
2447
                    foreach ($person->childFamilies() as $family) {
2448
                        foreach ($family->spouses() as $spouse) {
2449
                            $this->list[$spouse->xref()] = $spouse;
2450
                        }
2451
2452
                        foreach ($family->children() as $child) {
2453
                            $this->list[$child->xref()] = $child;
2454
                        }
2455
                    }
2456
                    break;
2457
                case 'spouse-family':
2458
                    foreach ($person->spouseFamilies() as $family) {
2459
                        foreach ($family->spouses() as $spouse) {
2460
                            $this->list[$spouse->xref()] = $spouse;
2461
                        }
2462
2463
                        foreach ($family->children() as $child) {
2464
                            $this->list[$child->xref()] = $child;
2465
                        }
2466
                    }
2467
                    break;
2468
                case 'direct-ancestors':
2469
                    $this->addAncestors($this->list, $id, false, $maxgen);
2470
                    break;
2471
                case 'ancestors':
2472
                    $this->addAncestors($this->list, $id, true, $maxgen);
2473
                    break;
2474
                case 'descendants':
2475
                    $this->list[$id]->generation = 1;
2476
                    $this->addDescendancy($this->list, $id, false, $maxgen);
2477
                    break;
2478
                case 'all':
2479
                    $this->addAncestors($this->list, $id, true, $maxgen);
2480
                    $this->addDescendancy($this->list, $id, true, $maxgen);
2481
                    break;
2482
            }
2483
        }
2484
2485
        switch ($sortby) {
2486
            case 'NAME':
2487
                uasort($this->list, GedcomRecord::nameComparator());
2488
                break;
2489
            case 'BIRT:DATE':
2490
                uasort($this->list, Individual::birthDateComparator());
2491
                break;
2492
            case 'DEAT:DATE':
2493
                uasort($this->list, Individual::deathDateComparator());
2494
                break;
2495
            case 'generation':
2496
                $newarray = [];
2497
                reset($this->list);
2498
                $genCounter = 1;
2499
                while (count($newarray) < count($this->list)) {
2500
                    foreach ($this->list as $key => $value) {
2501
                        $this->generation = $value->generation;
2502
                        if ($this->generation == $genCounter) {
2503
                            $newarray[$key] = (object) ['generation' => $this->generation];
2504
                        }
2505
                    }
2506
                    $genCounter++;
2507
                }
2508
                $this->list = $newarray;
2509
                break;
2510
            default:
2511
                // unsorted
2512
                break;
2513
        }
2514
        $this->repeats_stack[] = [$this->repeats, $this->repeat_bytes];
2515
        $this->repeat_bytes    = xml_get_current_line_number($this->parser) + 1;
2516
    }
2517
2518
    /**
2519
     * Handle </relatives>
2520
     *
2521
     * @return void
2522
     */
2523
    protected function relativesEndHandler(): void
2524
    {
2525
        $this->process_repeats--;
2526
        if ($this->process_repeats > 0) {
2527
            return;
2528
        }
2529
2530
        // Check if there is any relatives
2531
        if (count($this->list) > 0) {
2532
            $lineoffset = 0;
2533
            foreach ($this->repeats_stack as $rep) {
2534
                $lineoffset += $rep[1];
2535
            }
2536
            //-- read the xml from the file
2537
            $lines = file($this->report);
2538
            while (!str_contains($lines[$lineoffset + $this->repeat_bytes], '<Relatives') && $lineoffset + $this->repeat_bytes > 0) {
2539
                $lineoffset--;
2540
            }
2541
            $lineoffset++;
2542
            $reportxml = "<tempdoc>\n";
2543
            $line_nr   = $lineoffset + $this->repeat_bytes;
2544
            // Relatives Level counter
2545
            $count = 1;
2546
            while (0 < $count) {
2547
                if (str_contains($lines[$line_nr], '<Relatives')) {
2548
                    $count++;
2549
                } elseif (str_contains($lines[$line_nr], '</Relatives')) {
2550
                    $count--;
2551
                }
2552
                if (0 < $count) {
2553
                    $reportxml .= $lines[$line_nr];
2554
                }
2555
                $line_nr++;
2556
            }
2557
            // No need to drag this
2558
            unset($lines);
2559
            $reportxml .= "</tempdoc>\n";
2560
            // Save original values
2561
            $this->parser_stack[] = $this->parser;
2562
            $oldgedrec            = $this->gedrec;
2563
2564
            $this->list_total   = count($this->list);
2565
            $this->list_private = 0;
2566
            foreach ($this->list as $xref => $value) {
2567
                if (isset($value->generation)) {
2568
                    $this->generation = $value->generation;
2569
                }
2570
                $tmp          = Registry::gedcomRecordFactory()->make((string) $xref, $this->tree);
2571
                $this->gedrec = $tmp->privatizeGedcom(Auth::accessLevel($this->tree));
2572
2573
                $repeat_parser = xml_parser_create();
2574
                $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...
2575
                xml_parser_set_option($repeat_parser, XML_OPTION_CASE_FOLDING, false);
2576
2577
                xml_set_element_handler(
2578
                    $repeat_parser,
2579
                    function ($parser, string $name, array $attrs): void {
2580
                        $this->startElement($parser, $name, $attrs);
2581
                    },
2582
                    function ($parser, string $name): void {
2583
                        $this->endElement($parser, $name);
2584
                    }
2585
                );
2586
2587
                xml_set_character_data_handler(
2588
                    $repeat_parser,
2589
                    function ($parser, string $data): void {
2590
                        $this->characterData($parser, $data);
2591
                    }
2592
                );
2593
2594
                if (!xml_parse($repeat_parser, $reportxml, true)) {
2595
                    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)));
2596
                }
2597
                xml_parser_free($repeat_parser);
2598
            }
2599
            // Clean up the list array
2600
            $this->list   = [];
2601
            $this->parser = array_pop($this->parser_stack);
2602
            $this->gedrec = $oldgedrec;
2603
        }
2604
        [$this->repeats, $this->repeat_bytes] = array_pop($this->repeats_stack);
2605
    }
2606
2607
    /**
2608
     * Handle <generation />
2609
     * Prints the number of generations
2610
     *
2611
     * @return void
2612
     */
2613
    protected function generationStartHandler(): void
2614
    {
2615
        $this->current_element->addText((string) $this->generation);
2616
    }
2617
2618
    /**
2619
     * Handle <newPage />
2620
     * Has to be placed in an element (header, body or footer)
2621
     *
2622
     * @return void
2623
     */
2624
    protected function newPageStartHandler(): void
2625
    {
2626
        $temp = 'addpage';
2627
        $this->wt_report->addElement($temp);
2628
    }
2629
2630
    /**
2631
     * Handle </title>
2632
     *
2633
     * @return void
2634
     */
2635
    protected function titleEndHandler(): void
2636
    {
2637
        $this->report_root->addTitle($this->text);
2638
    }
2639
2640
    /**
2641
     * Handle </description>
2642
     *
2643
     * @return void
2644
     */
2645
    protected function descriptionEndHandler(): void
2646
    {
2647
        $this->report_root->addDescription($this->text);
2648
    }
2649
2650
    /**
2651
     * Create a list of all descendants.
2652
     *
2653
     * @param array<Individual> $list
2654
     * @param string            $pid
2655
     * @param bool              $parents
2656
     * @param int               $generations
2657
     *
2658
     * @return void
2659
     */
2660
    private function addDescendancy(&$list, $pid, $parents = false, $generations = -1): void
2661
    {
2662
        $person = Registry::individualFactory()->make($pid, $this->tree);
2663
        if ($person === null) {
2664
            return;
2665
        }
2666
        if (!isset($list[$pid])) {
2667
            $list[$pid] = $person;
2668
        }
2669
        if (!isset($list[$pid]->generation)) {
2670
            $list[$pid]->generation = 0;
2671
        }
2672
        foreach ($person->spouseFamilies() as $family) {
2673
            if ($parents) {
2674
                $husband = $family->husband();
2675
                $wife    = $family->wife();
2676
                if ($husband) {
2677
                    $list[$husband->xref()] = $husband;
2678
                    if (isset($list[$pid]->generation)) {
2679
                        $list[$husband->xref()]->generation = $list[$pid]->generation - 1;
2680
                    } else {
2681
                        $list[$husband->xref()]->generation = 1;
2682
                    }
2683
                }
2684
                if ($wife) {
2685
                    $list[$wife->xref()] = $wife;
2686
                    if (isset($list[$pid]->generation)) {
2687
                        $list[$wife->xref()]->generation = $list[$pid]->generation - 1;
2688
                    } else {
2689
                        $list[$wife->xref()]->generation = 1;
2690
                    }
2691
                }
2692
            }
2693
2694
            $children = $family->children();
2695
2696
            foreach ($children as $child) {
2697
                if ($child) {
2698
                    $list[$child->xref()] = $child;
2699
2700
                    if (isset($list[$pid]->generation)) {
2701
                        $list[$child->xref()]->generation = $list[$pid]->generation + 1;
2702
                    } else {
2703
                        $list[$child->xref()]->generation = 2;
2704
                    }
2705
                }
2706
            }
2707
            if ($generations == -1 || $list[$pid]->generation + 1 < $generations) {
2708
                foreach ($children as $child) {
2709
                    $this->addDescendancy($list, $child->xref(), $parents, $generations); // recurse on the childs family
2710
                }
2711
            }
2712
        }
2713
    }
2714
2715
    /**
2716
     * Create a list of all ancestors.
2717
     *
2718
     * @param array<Individual> $list
2719
     * @param string            $pid
2720
     * @param bool              $children
2721
     * @param int               $generations
2722
     *
2723
     * @return void
2724
     */
2725
    private function addAncestors(array &$list, string $pid, bool $children = false, int $generations = -1): void
2726
    {
2727
        $genlist                = [$pid];
2728
        $list[$pid]->generation = 1;
2729
        while (count($genlist) > 0) {
2730
            $id = array_shift($genlist);
2731
            if (str_starts_with($id, 'empty')) {
2732
                continue; // id can be something like “empty7”
2733
            }
2734
            $person = Registry::individualFactory()->make($id, $this->tree);
2735
            foreach ($person->childFamilies() as $family) {
2736
                $husband = $family->husband();
2737
                $wife    = $family->wife();
2738
                if ($husband) {
2739
                    $list[$husband->xref()]             = $husband;
2740
                    $list[$husband->xref()]->generation = $list[$id]->generation + 1;
2741
                }
2742
                if ($wife) {
2743
                    $list[$wife->xref()]             = $wife;
2744
                    $list[$wife->xref()]->generation = $list[$id]->generation + 1;
2745
                }
2746
                if ($generations == -1 || $list[$id]->generation + 1 < $generations) {
2747
                    if ($husband) {
2748
                        $genlist[] = $husband->xref();
2749
                    }
2750
                    if ($wife) {
2751
                        $genlist[] = $wife->xref();
2752
                    }
2753
                }
2754
                if ($children) {
2755
                    foreach ($family->children() as $child) {
2756
                        $list[$child->xref()] = $child;
2757
                        $list[$child->xref()]->generation = $list[$id]->generation ?? 1;
2758
                    }
2759
                }
2760
            }
2761
        }
2762
    }
2763
2764
    /**
2765
     * get gedcom tag value
2766
     *
2767
     * @param string $tag    The tag to find, use : to delineate subtags
2768
     * @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
2769
     * @param string $gedrec The gedcom record to get the value from
2770
     *
2771
     * @return string the value of a gedcom tag from the given gedcom record
2772
     */
2773
    private function getGedcomValue(string $tag, int $level, string $gedrec): string
2774
    {
2775
        if ($gedrec === '') {
2776
            return '';
2777
        }
2778
        $tags      = explode(':', $tag);
2779
        $origlevel = $level;
2780
        if ($level === 0) {
2781
            $level = $gedrec[0] + 1;
2782
        }
2783
2784
        $subrec = $gedrec;
2785
        $t = 'XXXX';
2786
        foreach ($tags as $t) {
2787
            $lastsubrec = $subrec;
2788
            $subrec     = self::getSubRecord($level, "$level $t", $subrec);
2789
            if (empty($subrec) && $origlevel == 0) {
2790
                $level--;
2791
                $subrec = self::getSubRecord($level, "$level $t", $lastsubrec);
2792
            }
2793
            if (empty($subrec)) {
2794
                if ($t === 'TITL') {
2795
                    $subrec = self::getSubRecord($level, "$level ABBR", $lastsubrec);
2796
                    if (!empty($subrec)) {
2797
                        $t = 'ABBR';
2798
                    }
2799
                }
2800
                if ($subrec === '') {
2801
                    if ($level > 0) {
2802
                        $level--;
2803
                    }
2804
                    $subrec = self::getSubRecord($level, "@ $t", $gedrec);
2805
                    if ($subrec === '') {
2806
                        return '';
2807
                    }
2808
                }
2809
            }
2810
            $level++;
2811
        }
2812
        $level--;
2813
        $ct = preg_match("/$level $t(.*)/", $subrec, $match);
2814
        if ($ct === 0) {
2815
            $ct = preg_match("/$level @.+@ (.+)/", $subrec, $match);
2816
        }
2817
        if ($ct === 0) {
2818
            $ct = preg_match("/@ $t (.+)/", $subrec, $match);
2819
        }
2820
        if ($ct > 0) {
2821
            $value = trim($match[1]);
2822
            if ($t === 'NOTE' && preg_match('/^@(.+)@$/', $value, $match)) {
2823
                $note = Registry::noteFactory()->make($match[1], $this->tree);
2824
                if ($note instanceof Note) {
2825
                    $value = $note->getNote();
2826
                } else {
2827
                    //-- set the value to the id without the @
2828
                    $value = $match[1];
2829
                }
2830
            }
2831
            if ($level !== 0 || $t !== 'NOTE') {
2832
                $value .= self::getCont($level + 1, $subrec);
2833
            }
2834
2835
            return $value;
2836
        }
2837
2838
        return '';
2839
    }
2840
2841
    /**
2842
     * Replace variable identifiers with their values.
2843
     *
2844
     * @param string $expression An expression such as "$foo == 123"
2845
     * @param bool   $quote      Whether to add quotation marks
2846
     *
2847
     * @return string
2848
     */
2849
    private function substituteVars($expression, $quote): string
2850
    {
2851
        return preg_replace_callback(
2852
            '/\$(\w+)/',
2853
            function (array $matches) use ($quote): string {
2854
                if (isset($this->vars[$matches[1]]['id'])) {
2855
                    if ($quote) {
2856
                        return "'" . addcslashes($this->vars[$matches[1]]['id'], "'") . "'";
2857
                    }
2858
2859
                    return $this->vars[$matches[1]]['id'];
2860
                }
2861
2862
                Log::addErrorLog(sprintf('Undefined variable $%s in report', $matches[1]));
2863
2864
                return '$' . $matches[1];
2865
            },
2866
            $expression
2867
        );
2868
    }
2869
}
2870