Passed
Push — dev ( 8976f1...f79383 )
by Greg
07:34
created

ReportParserGenerate::generationStartHandler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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