ReportParserGenerate::textStartHandler()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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