FunctionsPrintFacts::printFactSources()   B
last analyzed

Complexity

Conditions 10
Paths 45

Size

Total Lines 59
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 42
nc 45
nop 3
dl 0
loc 59
rs 7.6666
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 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\Functions;
21
22
use Fisharebest\Webtrees\Age;
23
use Fisharebest\Webtrees\Auth;
24
use Fisharebest\Webtrees\Contracts\UserInterface;
25
use Fisharebest\Webtrees\Date;
26
use Fisharebest\Webtrees\Elements\SubmitterText;
27
use Fisharebest\Webtrees\Elements\UnknownElement;
28
use Fisharebest\Webtrees\Fact;
29
use Fisharebest\Webtrees\Family;
30
use Fisharebest\Webtrees\Filter;
31
use Fisharebest\Webtrees\Gedcom;
32
use Fisharebest\Webtrees\GedcomRecord;
33
use Fisharebest\Webtrees\GedcomTag;
34
use Fisharebest\Webtrees\Http\RequestHandlers\EditFactPage;
35
use Fisharebest\Webtrees\I18N;
36
use Fisharebest\Webtrees\Individual;
37
use Fisharebest\Webtrees\Media;
38
use Fisharebest\Webtrees\Module\ModuleChartInterface;
39
use Fisharebest\Webtrees\Module\ModuleInterface;
40
use Fisharebest\Webtrees\Module\RelationshipsChartModule;
41
use Fisharebest\Webtrees\Note;
42
use Fisharebest\Webtrees\Registry;
43
use Fisharebest\Webtrees\Repository;
44
use Fisharebest\Webtrees\Services\ModuleService;
45
use Fisharebest\Webtrees\Services\UserService;
46
use Fisharebest\Webtrees\Submission;
47
use Fisharebest\Webtrees\Submitter;
48
use Fisharebest\Webtrees\Tree;
49
use Ramsey\Uuid\Uuid;
50
51
use function app;
52
use function array_merge;
53
use function count;
54
use function e;
55
use function explode;
56
use function implode;
57
use function ob_get_clean;
58
use function ob_start;
59
use function preg_match;
60
use function preg_match_all;
61
use function preg_replace;
62
use function preg_split;
63
use function rawurlencode;
64
use function route;
65
use function str_contains;
66
use function str_replace;
67
use function strip_tags;
68
use function strlen;
69
use function strpos;
70
use function substr;
71
use function trim;
72
use function view;
73
74
use const PREG_SET_ORDER;
75
76
/**
77
 * Class FunctionsPrintFacts - common functions
78
 *
79
 * @deprecated since 2.0.6.  Will be removed in 2.1.0
80
 */
81
class FunctionsPrintFacts
82
{
83
    /**
84
     * Print a fact record, for the individual/family/source/repository/etc. pages.
85
     * Although a Fact has a parent object, we also need to know
86
     * the GedcomRecord for which we are printing it. For example,
87
     * we can show the death of X on the page of Y, or the marriage
88
     * of X+Y on the page of Z. We need to know both records to
89
     * calculate ages, relationships, etc.
90
     *
91
     * @param Fact         $fact
92
     * @param GedcomRecord $record
93
     *
94
     * @return void
95
     */
96
    public static function printFact(Fact $fact, GedcomRecord $record): void
97
    {
98
        $parent = $fact->record();
99
        $tree   = $parent->tree();
100
        $tag    = $fact->getTag();
101
        $label  = $fact->label();
102
        $value  = $fact->value();
103
        $type   = $fact->attribute('TYPE');
104
        $id     = $fact->id();
105
106
        $element = Registry::elementFactory()->make($fact->tag());
107
108
        // This preference is named HIDE instead of SHOW
109
        $hide_errors = $tree->getPreference('HIDE_GEDCOM_ERRORS') === '0';
110
111
        // Some facts don't get printed here ...
112
        switch ($tag) {
113
            case 'NOTE':
114
                self::printMainNotes($fact, 1);
115
116
                return;
117
            case 'SOUR':
118
                self::printMainSources($fact, 1);
119
120
                return;
121
            case 'OBJE':
122
                self::printMainMedia($fact, 1);
123
124
                return;
125
            case 'FAMC':
126
            case 'FAMS':
127
            case 'CHIL':
128
            case 'HUSB':
129
            case 'WIFE':
130
                // These are internal links, not facts
131
                return;
132
            case '_WT_OBJE_SORT':
133
                // These links were once used internally to record the sort order.
134
                return;
135
            default:
136
                // Hide unrecognized/custom tags?
137
                if ($hide_errors && !GedcomTag::isTag($tag)) {
138
                    return;
139
                }
140
                break;
141
        }
142
143
        // New or deleted facts need different styling
144
        $styles = [];
145
        if ($fact->isPendingAddition()) {
146
            $styles[] = 'wt-new';
147
        }
148
        if ($fact->isPendingDeletion()) {
149
            $styles[] = 'wt-old';
150
        }
151
152
        // Event of close relative
153
        if ($tag === 'EVEN' && $value === 'CLOSE_RELATIVE') {
154
            $styles[] = 'wt-relation-fact collapse';
155
        }
156
157
        // Event of close associates
158
        if ($id === 'asso') {
159
            $styles[] = 'wt-relation-fact collapse';
160
        }
161
162
        // historical facts
163
        if ($id === 'histo') {
164
            $styles[] = 'wt-historic-fact collapse';
165
        }
166
167
        // Use marriage type as the label.  e.g. "Civil partnership"
168
        if ($tag === 'MARR') {
169
            $label = $fact->label();
170
            $type  = '';
171
        }
172
173
        echo '<tr class="', implode(' ', $styles), '">';
174
        echo '<th scope="row">';
175
        echo $label;
176
177
        if ($id !== 'histo' && $id !== 'asso' && $fact->canEdit()) {
178
            echo '<div class="editfacts nowrap">';
179
            echo view('edit/icon-fact-edit', ['fact' => $fact, 'url' => $record->url()]);
180
            echo view('edit/icon-fact-copy', ['fact' => $fact]);
181
            echo view('edit/icon-fact-delete', ['fact' => $fact]);
182
            echo '</div>';
183
        }
184
185
        if ($tree->getPreference('SHOW_FACT_ICONS')) {
186
            echo '<span class="wt-fact-icon wt-fact-icon-' . $tag . '" title="' . strip_tags($label) . '"></span>';
187
        }
188
189
        echo '</th>';
190
        echo '<td>';
191
192
        // Event from another record?
193
        if ($parent !== $record) {
194
            if ($parent instanceof Family) {
195
                foreach ($parent->spouses() as $spouse) {
196
                    if ($record !== $spouse) {
197
                        echo '<a href="', e($spouse->url()), '">', $spouse->fullName(), '</a> — ';
198
                    }
199
                }
200
                echo '<a href="', e($parent->url()), '">', I18N::translate('View this family'), '</a><br>';
201
            } elseif ($parent instanceof Individual) {
202
                echo '<a href="', e($parent->url()), '">', $parent->fullName(), '</a><br>';
203
            }
204
        }
205
206
        // Print the value of this fact/event
207
        switch ($tag) {
208
            case 'ADDR':
209
            case 'AFN':
210
            case 'LANG':
211
            case 'PUBL':
212
            case 'RESN':
213
                echo '<div class="field">' . $element->value($value, $tree) . '</div>';
214
                break;
215
            case 'ASSO':
216
                // we handle this later, in format_asso_rela_record()
217
                break;
218
            case 'EMAIL':
219
            case 'EMAI':
220
            case '_EMAIL':
221
                echo '<div class="field"><a href="mailto:', e($value), '">', e($value), '</a></div>';
222
                break;
223
            case 'REPO':
224
                $repository = $fact->target();
225
                if ($repository instanceof Repository) {
226
                    echo '<div><a class="field" href="', e($repository->url()), '">', $repository->fullName(), '</a></div>';
227
                } else {
228
                    echo '<div class="error">', e($value), '</div>';
229
                }
230
                break;
231
            case 'SUBM':
232
                $submitter = $fact->target();
233
                if ($submitter instanceof Submitter) {
234
                    echo '<div><a class="field" href="', e($submitter->url()), '">', $submitter->fullName(), '</a></div>';
235
                } else {
236
                    echo '<div class="error">', e($value), '</div>';
237
                }
238
                break;
239
            case 'SUBN':
240
                $submission = $fact->target();
241
                if ($submission instanceof Submission) {
242
                    echo '<div><a class="field" href="', e($submission->url()), '">', $submission->fullName(), '</a></div>';
243
                } else {
244
                    echo '<div class="error">', e($value), '</div>';
245
                }
246
                break;
247
            case 'URL':
248
            case '_URL':
249
            case 'WWW':
250
                echo '<div class="field"><a href="', e($value), '">', e($value), '</a></div>';
251
                break;
252
            case 'TEXT': // 0 SOUR / 1 TEXT
253
                echo Filter::formatText($value, $tree);
254
                break;
255
            case '_GOV':
256
                echo '<div class="field"><a href="https://gov.genealogy.net/item/show/', e($value), '">', e($value), '</a></div>';
257
                break;
258
            default:
259
                // Display the value for all other facts/events
260
                switch ($value) {
261
                    case '':
262
                    case 'CLOSE_RELATIVE':
263
                        // Nothing to display
264
                        break;
265
                    case 'N':
266
                        // Not valid GEDCOM
267
                        echo '<div class="field">', I18N::translate('No'), '</div>';
268
                        break;
269
                    case 'Y':
270
                        echo '<div class="field">', I18N::translate('Yes'), '</div>';
271
                        break;
272
                    default:
273
                        if (preg_match('/^@(' . Gedcom::REGEX_XREF . ')@$/', $value, $match)) {
274
                            $target = $fact->target();
275
                            if ($target instanceof GedcomRecord) {
276
                                echo '<div><a href="', e($target->url()), '">', $target->fullName(), '</a></div>';
277
                            } else {
278
                                echo '<div class="error">', e($value), '</div>';
279
                            }
280
                        } else {
281
                            echo '<div class="field"><span dir="auto">', e($value), '</span></div>';
282
                        }
283
                        break;
284
                }
285
                break;
286
        }
287
288
        // Print the type of this fact/event
289
        if ($type !== '' && $tag !== 'EVEN' && $tag !== 'FACT') {
290
            // Allow (custom) translations for other types
291
            $type = I18N::translate($type);
292
            echo GedcomTag::getLabelValue('TYPE', e($type));
293
        }
294
295
        // Print the date of this fact/event
296
        echo FunctionsPrint::formatFactDate($fact, $record, true, true);
297
298
        // Print the place of this fact/event
299
        echo '<div class="place">', FunctionsPrint::formatFactPlace($fact, true, true, true), '</div>';
300
        // A blank line between the primary attributes (value, date, place) and the secondary ones
301
        echo '<br>';
302
303
        $addr = $fact->attribute('ADDR');
304
        if ($addr !== '') {
305
            $addr = e($addr);
306
            if (str_contains($addr, "\n")) {
307
                $addr = '<span class="d-block" style="white-space: pre-wrap">' . $addr . '</span';
308
            }
309
310
            echo GedcomTag::getLabelValue($fact->tag() . ':ADDR', $addr);
311
        }
312
313
        // Print the associates of this fact/event
314
        if ($id !== 'asso') {
315
            echo self::formatAssociateRelationship($fact);
316
        }
317
318
        // Print any other "2 XXXX" attributes, in the order in which they appear.
319
        preg_match_all('/\n2 (' . Gedcom::REGEX_TAG . ') (.+)/', $fact->gedcom(), $matches, PREG_SET_ORDER);
320
321
        //0 SOUR / 1 DATA / 2 EVEN / 3 DATE and 3 PLAC must be collected separately
322
        preg_match_all('/\n2 EVEN .*((\n[3].*)*)/', $fact->gedcom(), $evenMatches, PREG_SET_ORDER);
323
        $currentEvenMatch = 0;
324
325
        foreach ($matches as $match) {
326
            switch ($match[1]) {
327
                case 'DATE':
328
                case 'TIME':
329
                case 'AGE':
330
                case 'HUSB':
331
                case 'WIFE':
332
                case 'PLAC':
333
                case 'ADDR':
334
                case 'ALIA':
335
                case 'ASSO':
336
                case '_ASSO':
337
                case 'DESC':
338
                case 'RELA':
339
                case 'STAT':
340
                case 'TEMP':
341
                case 'TYPE':
342
                case 'FAMS':
343
                case 'CONT':
344
                    // These were already shown at the beginning
345
                    break;
346
                case 'NOTE':
347
                case 'OBJE':
348
                case 'SOUR':
349
                    // These will be shown at the end
350
                    break;
351
                case '_UID':
352
                case 'RIN':
353
                    // These don't belong at level 2, so do not display them.
354
                    // They are only shown when editing.
355
                    break;
356
                case 'EVEN': // 0 SOUR / 1 DATA / 2 EVEN / 3 DATE / 3 PLAC
357
                    $events = [];
358
                    foreach (preg_split('/ *, */', $match[2]) as $event) {
359
                        $events[] = GedcomTag::getLabel($event);
360
                    }
361
                    echo GedcomTag::getLabelValue('EVEN', implode(I18N::$list_separator, $events));
362
363
                    if (preg_match('/\n3 DATE (.+)/', $evenMatches[$currentEvenMatch][0], $date_match)) {
364
                        $date = new Date($date_match[1]);
365
                        echo GedcomTag::getLabelValue('DATE', $date->display());
366
                    }
367
                    if (preg_match('/\n3 PLAC (.+)/', $evenMatches[$currentEvenMatch][0], $plac_match)) {
368
                        echo GedcomTag::getLabelValue('PLAC', $plac_match[1]);
369
                    }
370
                    $currentEvenMatch++;
371
372
                    break;
373
                case 'FAMC': // 0 INDI / 1 ADOP / 2 FAMC / 3 ADOP
374
                    $family = Registry::familyFactory()->make(str_replace('@', '', $match[2]), $tree);
375
                    if ($family instanceof Family) {
376
                        echo GedcomTag::getLabelValue('FAM', '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>');
377
                        if (preg_match('/\n3 ADOP (HUSB|WIFE|BOTH)/', $fact->gedcom(), $adop_match)) {
378
                            echo Registry::elementFactory()->make('INDI:ADOP:FAMC:ADOP')->labelValue($adop_match[1], $tree);
379
                        }
380
                    } else {
381
                        echo GedcomTag::getLabelValue('FAM', '<span class="error">' . $match[2] . '</span>');
382
                    }
383
                    break;
384
                case '_WT_USER':
385
                    if (Auth::check()) {
386
                        $user = (new UserService())->findByIdentifier($match[2]); // may not exist
387
                        if ($user instanceof UserInterface) {
388
                            echo GedcomTag::getLabelValue('_WT_USER', '<span dir="auto">' . e($user->realName()) . '</span>');
389
                        } else {
390
                            echo GedcomTag::getLabelValue('_WT_USER', e($match[2]));
391
                        }
392
                    }
393
                    break;
394
                case 'RESN':
395
                    switch ($match[2]) {
396
                        case 'none':
397
                            // Note: "2 RESN none" is not valid gedcom.
398
                            // However, webtrees privacy rules will interpret it as "show an otherwise private fact to public".
399
                            echo GedcomTag::getLabelValue('RESN', '<i class="icon-resn-none"></i> ' . I18N::translate('Show to visitors'));
400
                            break;
401
                        case 'privacy':
402
                            echo GedcomTag::getLabelValue('RESN', '<i class="icon-resn-privacy"></i> ' . I18N::translate('Show to members'));
403
                            break;
404
                        case 'confidential':
405
                            echo GedcomTag::getLabelValue('RESN', '<i class="icon-resn-confidential"></i> ' . I18N::translate('Show to managers'));
406
                            break;
407
                        case 'locked':
408
                            echo GedcomTag::getLabelValue('RESN', '<i class="icon-resn-locked"></i> ' . I18N::translate('Only managers can edit'));
409
                            break;
410
                        default:
411
                            echo GedcomTag::getLabelValue('RESN', e($match[2]));
412
                            break;
413
                    }
414
                    break;
415
                case 'CALN':
416
                    echo GedcomTag::getLabelValue('CALN', Filter::expandUrls($match[2], $tree));
417
                    break;
418
                case 'URL':
419
                case '_URL':
420
                case 'WWW':
421
                    $link = '<a href="' . e($match[2]) . '">' . e($match[2]) . '</a>';
422
                    echo GedcomTag::getLabelValue($tag . ':' . $match[1], $link);
423
                    break;
424
                default:
425
                    if (!$hide_errors || GedcomTag::isTag($match[1])) {
426
                        if (preg_match('/^@(' . Gedcom::REGEX_XREF . ')@$/', $match[2], $xmatch)) {
427
                            // Links
428
                            $linked_record = Registry::gedcomRecordFactory()->make($xmatch[1], $tree);
429
                            if ($linked_record) {
430
                                $link = '<a href="' . e($linked_record->url()) . '">' . $linked_record->fullName() . '</a>';
431
                                echo GedcomTag::getLabelValue($tag . ':' . $match[1], $link);
432
                            } else {
433
                                echo GedcomTag::getLabelValue($tag . ':' . $match[1], e($match[2]));
434
                            }
435
                        } else {
436
                            // Non links
437
                            echo GedcomTag::getLabelValue($tag . ':' . $match[1], e($match[2]));
438
                        }
439
                    }
440
                    break;
441
            }
442
        }
443
        echo self::printFactSources($tree, $fact->gedcom(), 2);
444
        echo FunctionsPrint::printFactNotes($tree, $fact->gedcom(), 2);
445
        self::printMediaLinks($tree, $fact->gedcom(), 2);
446
        echo '</td></tr>';
447
    }
448
449
    /**
450
     * Print the associations from the associated individuals in $event to the individuals in $record
451
     *
452
     * @param Fact $event
453
     *
454
     * @return string
455
     */
456
    private static function formatAssociateRelationship(Fact $event): string
457
    {
458
        $parent = $event->record();
459
        // To whom is this record an assocate?
460
        if ($parent instanceof Individual) {
461
            // On an individual page, we just show links to the person
462
            $associates = [$parent];
463
        } elseif ($parent instanceof Family) {
464
            // On a family page, we show links to both spouses
465
            $associates = $parent->spouses();
466
        } else {
467
            // On other pages, it does not make sense to show associates
468
            return '';
469
        }
470
471
        preg_match_all('/^1 (ASSO) @(' . Gedcom::REGEX_XREF . ')@((\n[2-9].*)*)/', $event->gedcom(), $amatches1, PREG_SET_ORDER);
472
        preg_match_all('/\n2 (_?ASSO) @(' . Gedcom::REGEX_XREF . ')@((\n[3-9].*)*)/', $event->gedcom(), $amatches2, PREG_SET_ORDER);
473
474
        $html = '';
475
        // For each ASSO record
476
        foreach (array_merge($amatches1, $amatches2) as $amatch) {
477
            $person = Registry::individualFactory()->make($amatch[2], $event->record()->tree());
478
            if ($person && $person->canShowName()) {
479
                // Is there a "RELA" tag
480
                if (preg_match('/\n([23]) RELA (.+)/', $amatch[3], $rmatch)) {
481
                    if ($rmatch[1] === '2') {
482
                        $rela_tag = $event->record()->tag() . ':' . $amatch[1] . ':RELA';
483
                    } else {
484
                        $rela_tag = $event->tag() . ':' . $amatch[1] . ':RELA';
485
                    }
486
                    // Use the supplied relationship as a label
487
                    $label = Registry::elementFactory()->make($rela_tag)->value($rmatch[2], $parent->tree());
488
                } elseif (preg_match('/^1 _?ASSO/', $event->gedcom())) {
489
                    // Use a default label
490
                    $label = Registry::elementFactory()->make($event->tag())->label();
491
                } else {
492
                    // Use a default label
493
                    $label = Registry::elementFactory()->make($event->tag() . ':_ASSO')->label();
494
                }
495
496
                if ($person->getBirthDate()->isOK() && $event->date()->isOK()) {
497
                    $age = new Age($person->getBirthDate(), $event->date());
498
                    switch ($person->sex()) {
499
                        case 'M':
500
                            $age_text = ' ' . I18N::translateContext('Male', '(aged %s)', (string) $age);
501
                            break;
502
                        case 'F':
503
                            $age_text = ' ' . I18N::translateContext('Female', '(aged %s)', (string) $age);
504
                            break;
505
                        default:
506
                            $age_text = ' ' . I18N::translate('(aged %s)', (string) $age);
507
                            break;
508
                    }
509
                } else {
510
                    $age_text = '';
511
                }
512
513
                $values = ['<a href="' . e($person->url()) . '">' . $person->fullName() . '</a>' . $age_text];
514
515
                $module = app(ModuleService::class)->findByComponent(ModuleChartInterface::class, $person->tree(), Auth::user())->first(static function (ModuleInterface $module) {
516
                    return $module instanceof RelationshipsChartModule;
517
                });
518
519
                if ($module instanceof RelationshipsChartModule) {
520
                    foreach ($associates as $associate) {
521
                        $relationship_name = Functions::getCloseRelationshipName($associate, $person);
522
                        if ($relationship_name === '') {
523
                            $relationship_name = GedcomTag::getLabel('RELA');
524
                        }
525
526
                        if ($parent instanceof Family) {
527
                            // For family ASSO records (e.g. MARR), identify the spouse with a sex icon
528
                            $relationship_name .= '<small>' . view('icons/sex', ['sex' => $associate->sex()]) . '</small>';
529
                        }
530
531
                        $values[] = '<a href="' . $module->chartUrl($associate, ['xref2' => $person->xref()]) . '" rel="nofollow">' . $relationship_name . '</a>';
532
                    }
533
                }
534
                $value = implode(' — ', $values);
535
536
                // Use same markup as GedcomTag::getLabelValue()
537
                $asso = I18N::translate('<span class="label">%1$s:</span> <span class="field" dir="auto">%2$s</span>', $label, $value);
538
            } elseif (!$person && Auth::isEditor($event->record()->tree())) {
539
                $asso = GedcomTag::getLabelValue('ASSO', '<span class="error">' . $amatch[1] . '</span>');
540
            } else {
541
                $asso = '';
542
            }
543
            $html .= '<div class="fact_ASSO">' . $asso . '</div>';
544
        }
545
546
        return $html;
547
    }
548
549
    /**
550
     * print a source linked to a fact (2 SOUR)
551
     * this function is called by the FunctionsPrintFacts::print_fact function and other functions to
552
     * print any source information attached to the fact
553
     *
554
     * @param Tree   $tree
555
     * @param string $factrec The fact record to look for sources in
556
     * @param int    $level   The level to look for sources at
557
     *
558
     * @return string HTML text
559
     */
560
    public static function printFactSources(Tree $tree, string $factrec, int $level): string
561
    {
562
        $data   = '';
563
        $nlevel = $level + 1;
564
565
        // Systems not using source records
566
        // The old style is not supported when entering or editing sources, but may be found in imported trees.
567
        // Also, the old style sources allow histo.* files to use tree independent source citations, which
568
        // will display nicely when markdown is used.
569
        $ct = preg_match_all('/' . $level . ' SOUR (.*)((?:\n\d CONT.*)*)/', $factrec, $match, PREG_SET_ORDER);
570
        for ($j = 0; $j < $ct; $j++) {
571
            if (!str_contains($match[$j][1], '@')) {
572
                $source = e($match[$j][1] . preg_replace('/\n\d CONT ?/', "\n", $match[$j][2]));
573
                $data   .= '<div class="fact_SOUR"><span class="label">' . I18N::translate('Source') . ':</span> <span class="field" dir="auto">' . Filter::formatText($source, $tree) . '</span></div>';
574
            }
575
        }
576
        // Find source for each fact
577
        $ct    = preg_match_all("/$level SOUR @(.*)@/", $factrec, $match, PREG_SET_ORDER);
578
        $spos2 = 0;
579
        for ($j = 0; $j < $ct; $j++) {
580
            $sid    = $match[$j][1];
581
            $source = Registry::sourceFactory()->make($sid, $tree);
582
            if ($source) {
583
                if ($source->canShow()) {
584
                    $spos1 = strpos($factrec, "$level SOUR @" . $sid . '@', $spos2);
585
                    $spos2 = strpos($factrec, "\n$level", $spos1);
586
                    if (!$spos2) {
587
                        $spos2 = strlen($factrec);
588
                    }
589
                    $srec     = substr($factrec, $spos1, $spos2 - $spos1);
590
                    $lt       = preg_match_all("/$nlevel \w+/", $srec, $matches);
591
                    $data     .= '<div class="fact_SOUR">';
592
                    $id       = 'collapse-' . Uuid::uuid4()->toString();
593
                    $expanded = (bool) $tree->getPreference('EXPAND_SOURCES');
594
                    if ($lt > 0) {
595
                        $data .= '<a href="#' . e($id) . '" role="button" data-toggle="collapse" aria-controls="' . e($id) . '" aria-expanded="' . ($expanded ? 'true' : 'false') . '">';
596
                        $data .= view('icons/expand');
597
                        $data .= view('icons/collapse');
598
                        $data .= '</a>';
599
                    }
600
                    $data .= GedcomTag::getLabelValue('SOUR', '<a href="' . e($source->url()) . '">' . $source->fullName() . '</a>', null, 'span');
601
                    $data .= '</div>';
602
603
                    $data .= '<div id="' . e($id) . '" class="collapse ' . ($expanded ? 'show' : '') . '">';
604
                    $data .= self::printSourceStructure($tree, self::getSourceStructure($srec));
605
                    $data .= '<div class="indent">';
606
                    ob_start();
607
                    self::printMediaLinks($tree, $srec, $nlevel);
608
                    $data .= ob_get_clean();
609
                    $data .= FunctionsPrint::printFactNotes($tree, $srec, $nlevel);
610
                    $data .= '</div>';
611
                    $data .= '</div>';
612
                }
613
            } else {
614
                $data .= GedcomTag::getLabelValue('SOUR', '<span class="error">' . $sid . '</span>');
615
            }
616
        }
617
618
        return $data;
619
    }
620
621
    /**
622
     * Print the links to media objects
623
     *
624
     * @param Tree   $tree
625
     * @param string $factrec
626
     * @param int    $level
627
     *
628
     * @return void
629
     */
630
    public static function printMediaLinks(Tree $tree, string $factrec, int $level): void
631
    {
632
        $nlevel = $level + 1;
633
        if (preg_match_all("/$level OBJE @(.*)@/", $factrec, $omatch, PREG_SET_ORDER) === 0) {
634
            return;
635
        }
636
        $objectNum = 0;
637
        while ($objectNum < count($omatch)) {
638
            $media_id = $omatch[$objectNum][1];
639
            $media    = Registry::mediaFactory()->make($media_id, $tree);
640
            if ($media) {
641
                if ($media->canShow()) {
642
                    echo '<div class="d-flex align-items-center"><div class="p-1">';
643
                    foreach ($media->mediaFiles() as $media_file) {
644
                        echo $media_file->displayImage(100, 100, 'contain', []);
645
                    }
646
                    echo '</div>';
647
                    echo '<div>';
648
                    echo '<a href="', e($media->url()), '">', $media->fullName(), '</a>';
649
                    // NOTE: echo the notes of the media
650
                    echo '<p>';
651
                    echo FunctionsPrint::printFactNotes($tree, $media->gedcom(), 1);
652
                    //-- print spouse name for marriage events
653
                    echo FunctionsPrint::printFactNotes($tree, $media->gedcom(), $nlevel);
654
                    echo self::printFactSources($tree, $media->gedcom(), $nlevel);
655
                    echo '</div>'; //close div "media-display-title"
656
                    echo '</div>'; //close div "media-display"
657
                }
658
            } elseif ($tree->getPreference('HIDE_GEDCOM_ERRORS') === '1') {
659
                echo '<p class="alert alert-danger">', $media_id, '</p>';
660
            }
661
            $objectNum++;
662
        }
663
    }
664
665
    /**
666
     * Print a row for the sources tab on the individual page.
667
     *
668
     * @param Fact $fact
669
     * @param int  $level
670
     *
671
     * @return void
672
     */
673
    public static function printMainSources(Fact $fact, int $level): void
674
    {
675
        $factrec = $fact->gedcom();
676
        $parent  = $fact->record();
677
        $tree    = $fact->record()->tree();
678
679
        $nlevel = $level + 1;
680
        if ($fact->isPendingAddition()) {
681
            $styleadd = 'wt-new';
682
            $can_edit = $level === 1 && $fact->canEdit();
683
        } elseif ($fact->isPendingDeletion()) {
684
            $styleadd = 'wt-old';
685
            $can_edit = false;
686
        } else {
687
            $styleadd = '';
688
            $can_edit = $level === 1 && $fact->canEdit();
689
        }
690
691
        // -- find source for each fact
692
        preg_match_all('/(?:^|\n)(' . $level . ' SOUR (.*)(?:\n[' . $nlevel . '-9] .*)*)/', $fact->gedcom(), $matches, PREG_SET_ORDER);
693
694
        foreach ($matches as $match) {
695
            $srec   = $match[1];
696
            $sid    = $match[2];
697
            $source = Registry::sourceFactory()->make(trim($sid, '@'), $tree);
698
            // Allow access to "1 SOUR @non_existent_source@", so it can be corrected/deleted
699
            if (!$source || $source->canShow()) {
700
                if ($level > 1) {
701
                    echo '<tr class="wt-level-two-source collapse">';
702
                } else {
703
                    echo '<tr>';
704
                }
705
                echo '<th class="';
706
                if ($level > 1) {
707
                    echo ' rela';
708
                }
709
                echo ' ', $styleadd, '">';
710
                $factlines = explode("\n", $factrec); // 1 BIRT Y\n2 SOUR ...
711
                $factwords = explode(' ', $factlines[0]); // 1 BIRT Y
712
                $factname  = $factwords[1]; // BIRT
713
                if ($factname === 'EVEN' || $factname === 'FACT') {
714
                    // Add ' EVEN' to provide sensible output for an event with an empty TYPE record
715
                    $ct = preg_match('/2 TYPE (.*)/', $factrec, $ematch);
716
                    if ($ct > 0) {
717
                        $factname = trim($ematch[1]);
718
                        echo $factname;
719
                    } else {
720
                        echo GedcomTag::getLabel($factname);
721
                    }
722
                } elseif ($can_edit) {
723
                    echo '<a href="' . e(route(EditFactPage::class, [
724
                            'xref'    => $parent->xref(),
725
                            'fact_id' => $fact->id(),
726
                            'tree'    => $tree->name(),
727
                        ])) . '" title="', I18N::translate('Edit'), '">';
728
                    echo GedcomTag::getLabel($factname), '</a>';
729
                    echo '<div class="editfacts nowrap">';
730
                    if (preg_match('/^@.+@$/', $sid)) {
731
                        // Inline sources can't be edited. Attempting to save one will convert it
732
                        // into a link, and delete it.
733
                        // e.g. "1 SOUR my source" becomes "1 SOUR @my source@" which does not exist.
734
                        echo view('edit/icon-fact-edit', ['fact' => $fact]);
735
                        echo view('edit/icon-fact-copy', ['fact' => $fact]);
736
                    }
737
                    echo view('edit/icon-fact-delete', ['fact' => $fact]);
738
                } else {
739
                    echo GedcomTag::getLabel($factname);
740
                }
741
                echo '</th>';
742
                echo '<td class="', $styleadd, '">';
743
                if ($source) {
744
                    echo '<a href="', e($source->url()), '">', $source->fullName(), '</a>';
745
                    // 2 RESN tags. Note, there can be more than one, such as "privacy" and "locked"
746
                    if (preg_match_all("/\n2 RESN (.+)/", $factrec, $rmatches)) {
747
                        foreach ($rmatches[1] as $rmatch) {
748
                            echo '<br><span class="label">', GedcomTag::getLabel('RESN'), ':</span> <span class="field">';
749
                            switch ($rmatch) {
750
                                case 'none':
751
                                    // Note: "2 RESN none" is not valid gedcom, and the GUI will not let you add it.
752
                                    // However, webtrees privacy rules will interpret it as "show an otherwise private fact to public".
753
                                    echo '<i class="icon-resn-none"></i> ', I18N::translate('Show to visitors');
754
                                    break;
755
                                case 'privacy':
756
                                    echo '<i class="icon-resn-privacy"></i> ', I18N::translate('Show to members');
757
                                    break;
758
                                case 'confidential':
759
                                    echo '<i class="icon-resn-confidential"></i> ', I18N::translate('Show to managers');
760
                                    break;
761
                                case 'locked':
762
                                    echo '<i class="icon-resn-locked"></i> ', I18N::translate('Only managers can edit');
763
                                    break;
764
                                default:
765
                                    echo $rmatch;
766
                                    break;
767
                            }
768
                            echo '</span>';
769
                        }
770
                    }
771
                    $cs = preg_match("/$nlevel EVEN (.*)/", $srec, $cmatch);
772
                    if ($cs > 0) {
773
                        echo '<br><span class="label">', GedcomTag::getLabel('EVEN'), ' </span><span class="field">', $cmatch[1], '</span>';
774
                        $cs = preg_match('/' . ($nlevel + 1) . ' ROLE (.*)/', $srec, $cmatch);
775
                        if ($cs > 0) {
776
                            echo '<br>&nbsp;&nbsp;&nbsp;&nbsp;<span class="label">', GedcomTag::getLabel('ROLE'), ' </span><span class="field">', $cmatch[1], '</span>';
777
                        }
778
                    }
779
                    echo self::printSourceStructure($tree, self::getSourceStructure($srec));
780
                    echo '<div class="indent">';
781
                    self::printMediaLinks($tree, $srec, $nlevel);
782
                    if ($nlevel === 2) {
783
                        self::printMediaLinks($tree, $source->gedcom(), 1);
784
                    }
785
                    echo FunctionsPrint::printFactNotes($tree, $srec, $nlevel);
786
                    if ($nlevel === 2) {
787
                        echo FunctionsPrint::printFactNotes($tree, $source->gedcom(), 1);
788
                    }
789
                    echo '</div>';
790
                } else {
791
                    echo $sid;
792
                }
793
                echo '</td></tr>';
794
            }
795
        }
796
    }
797
798
    /**
799
     * Print SOUR structure
800
     *  This function prints the input array of SOUR sub-records built by the
801
     *  getSourceStructure() function.
802
     *
803
     * @param Tree                $tree
804
     * @param string[]|string[][] $textSOUR
805
     *
806
     * @return string
807
     */
808
    public static function printSourceStructure(Tree $tree, array $textSOUR): string
809
    {
810
        $html = '';
811
812
        if ($textSOUR['PAGE'] !== '') {
813
            $html .= Registry::elementFactory()->make('INDI:SOUR:PAGE')->labelValue($textSOUR['PAGE'], $tree);
814
        }
815
816
        if ($textSOUR['EVEN'] !== '') {
817
            $html .= Registry::elementFactory()->make('INDI:SOUR:EVEN')->labelValue($textSOUR['EVEN'], $tree);
818
819
            if ($textSOUR['ROLE']) {
820
                $html .= Registry::elementFactory()->make('INDI:SOUR:EVEN:ROLE')->labelValue($textSOUR['ROLE'], $tree);
821
            }
822
        }
823
824
        if ($textSOUR['DATE'] !== '') {
825
            $html .= Registry::elementFactory()->make('INDI:SOUR:DATA:DATE')->labelValue($textSOUR['DATE'], $tree);
826
        }
827
828
        foreach ($textSOUR['TEXT'] as $text) {
829
            $html .= Registry::elementFactory()->make('INDI:SOUR:DATA:TEXT')->labelValue($text, $tree);
830
        }
831
832
        if ($textSOUR['QUAY'] !== '') {
833
            $html .= Registry::elementFactory()->make('INDI:SOUR:QUAY')->labelValue($textSOUR['QUAY'], $tree);
834
        }
835
836
        return '<div class="indent">' . $html . '</div>';
837
    }
838
839
    /**
840
     * Extract SOUR structure from the incoming Source sub-record
841
     * The output array is defined as follows:
842
     *  $textSOUR['PAGE'] = Source citation
843
     *  $textSOUR['EVEN'] = Event type
844
     *  $textSOUR['ROLE'] = Role in event
845
     *  $textSOUR['DATA'] = place holder (no text in this sub-record)
846
     *  $textSOUR['DATE'] = Entry recording date
847
     *  $textSOUR['TEXT'] = (array) Text from source
848
     *  $textSOUR['QUAY'] = Certainty assessment
849
     *
850
     * @param string $srec
851
     *
852
     * @return array<array<string>>
853
     */
854
    public static function getSourceStructure(string $srec): array
855
    {
856
        // Set up the output array
857
        $textSOUR = [
858
            'PAGE' => '',
859
            'EVEN' => '',
860
            'ROLE' => '',
861
            'DATE' => '',
862
            'TEXT' => [],
863
            'QUAY' => '',
864
        ];
865
866
        preg_match_all('/^\d (PAGE|EVEN|ROLE|DATE|TEXT|QUAY) ?(.*(\n\d CONT.*)*)$/m', $srec, $matches, PREG_SET_ORDER);
867
868
        foreach ($matches as $match) {
869
            $tag   = $match[1];
870
            $value = $match[2];
871
            $value = preg_replace('/\n\d CONT ?/', "\n", $value);
872
873
            if ($tag === 'TEXT') {
874
                $textSOUR[$tag][] = $value;
875
            } else {
876
                $textSOUR[$tag] = $value;
877
            }
878
        }
879
880
        return $textSOUR;
881
    }
882
883
    /**
884
     * Print a row for the notes tab on the individual page.
885
     *
886
     * @param Fact $fact
887
     * @param int  $level
888
     *
889
     * @return void
890
     */
891
    public static function printMainNotes(Fact $fact, int $level): void
892
    {
893
        $factrec = $fact->gedcom();
894
        $parent  = $fact->record();
895
        $tree    = $parent->tree();
896
897
        if ($fact->isPendingAddition()) {
898
            $styleadd = 'wt-new ';
899
            $can_edit = $level === 1 && $fact->canEdit();
900
        } elseif ($fact->isPendingDeletion()) {
901
            $styleadd = 'wt-old ';
902
            $can_edit = false;
903
        } else {
904
            $styleadd = '';
905
            $can_edit = $level === 1 && $fact->canEdit();
906
        }
907
908
        $ct = preg_match_all("/$level NOTE (.*)/", $factrec, $match, PREG_SET_ORDER);
909
        for ($j = 0; $j < $ct; $j++) {
910
            // Note object, or inline note?
911
            if (preg_match("/$level NOTE @(.*)@/", $match[$j][0], $nmatch)) {
912
                $note = Registry::noteFactory()->make($nmatch[1], $tree);
913
                if ($note && !$note->canShow()) {
914
                    continue;
915
                }
916
            } else {
917
                $note = null;
918
            }
919
920
            if ($level >= 2) {
921
                echo '<tr class="wt-level-two-note collapse"><th scope="row" class="rela ', $styleadd, '">';
922
            } else {
923
                echo '<tr><th scope="row" class="', $styleadd, '">';
924
            }
925
            if ($can_edit) {
926
                if ($level < 2) {
927
                    if ($note instanceof Note) {
928
                        echo '<a href="' . e($note->url()) . '">';
929
                        echo GedcomTag::getLabel('SHARED_NOTE');
930
                        echo view('icons/note');
931
                        echo '</a>';
932
                    } else {
933
                        echo GedcomTag::getLabel('NOTE');
934
                    }
935
                    echo '<div class="editfacts nowrap">';
936
                    echo view('edit/icon-fact-edit', ['fact' => $fact]);
937
                    echo view('edit/icon-fact-copy', ['fact' => $fact]);
938
                    echo view('edit/icon-fact-delete', ['fact' => $fact]);
939
                    echo '</div>';
940
                }
941
            } else {
942
                if ($level < 2) {
943
                    if ($note) {
944
                        echo GedcomTag::getLabel('SHARED_NOTE');
945
                    } else {
946
                        echo GedcomTag::getLabel('NOTE');
947
                    }
948
                }
949
                $factlines = explode("\n", $factrec); // 1 BIRT Y\n2 NOTE ...
950
                $factwords = explode(' ', $factlines[0]); // 1 BIRT Y
951
                $factname  = $factwords[1]; // BIRT
952
                if ($factname === 'EVEN' || $factname === 'FACT') {
953
                    // Add ' EVEN' to provide sensible output for an event with an empty TYPE record
954
                    $ct = preg_match('/2 TYPE (.*)/', $factrec, $ematch);
955
                    if ($ct > 0) {
956
                        $factname = trim($ematch[1]);
957
                        echo $factname;
958
                    } else {
959
                        echo GedcomTag::getLabel($factname);
960
                    }
961
                } elseif ($factname !== 'NOTE') {
962
                    // Note is already printed
963
                    echo GedcomTag::getLabel($factname);
964
                    if ($note) {
965
                        echo '<a class="btn btn-link" href="' . e($note->url()) . '" title="' . I18N::translate('View') . '"><span class="sr-only">' . I18N::translate('View') . '</span></a>';
966
                    }
967
                }
968
            }
969
            echo '</th>';
970
            if ($note instanceof Note) {
971
                $text = $note->getNote();
972
            } else {
973
                $nrec = Functions::getSubRecord($level, "$level NOTE", $factrec, $j + 1);
974
                $text = $match[$j][1] . Functions::getCont($level + 1, $nrec);
975
            }
976
977
            $element = new SubmitterText('');
978
979
            echo '<td class="', $styleadd, ' wrap">';
980
            echo $element->value($text, $tree);
981
            echo '</td></tr>';
982
        }
983
    }
984
985
    /**
986
     * Print a row for the media tab on the individual page.
987
     *
988
     * @param Fact $fact
989
     * @param int  $level
990
     *
991
     * @return void
992
     */
993
    public static function printMainMedia(Fact $fact, int $level): void
994
    {
995
        $tree = $fact->record()->tree();
996
997
        if ($fact->isPendingAddition()) {
998
            $styleadd = 'wt-new';
999
        } elseif ($fact->isPendingDeletion()) {
1000
            $styleadd = 'wt-old';
1001
        } else {
1002
            $styleadd = '';
1003
        }
1004
1005
        // -- find source for each fact
1006
        preg_match_all('/(?:^|\n)' . $level . ' OBJE @(.*)@/', $fact->gedcom(), $matches);
1007
        foreach ($matches[1] as $xref) {
1008
            $media = Registry::mediaFactory()->make($xref, $tree);
1009
            // Allow access to "1 OBJE @non_existent_source@", so it can be corrected/deleted
1010
            if (!$media instanceof Media || $media->canShow()) {
1011
                echo '<tr class="', $styleadd, '">';
1012
                echo '<th scope="row">';
1013
                echo $fact->label();
1014
1015
                if ($level === 1 && $fact->canEdit()) {
1016
                    echo '<div class="editfacts nowrap">';
1017
                    echo view('edit/icon-fact-copy', ['fact' => $fact]);
1018
                    echo view('edit/icon-fact-delete', ['fact' => $fact]);
1019
                    echo '</div>';
1020
                }
1021
1022
                echo '</th>';
1023
                echo '<td>';
1024
                if ($media instanceof Media) {
1025
                    foreach ($media->mediaFiles() as $media_file) {
1026
                        echo '<div>';
1027
                        echo $media_file->displayImage(100, 100, 'contain', []);
1028
                        echo '<br>';
1029
                        echo '<a href="' . e($media->url()) . '"> ';
1030
                        echo '<em>';
1031
                        echo e($media_file->title() ?: $media_file->filename());
1032
                        echo '</em>';
1033
                        echo '</a>';
1034
                        echo '</div>';
1035
                    }
1036
1037
                    echo FunctionsPrint::printFactNotes($tree, $media->gedcom(), 1);
1038
                    echo self::printFactSources($tree, $media->gedcom(), 1);
1039
                } else {
1040
                    echo $xref;
1041
                }
1042
                echo '</td></tr>';
1043
            }
1044
        }
1045
    }
1046
}
1047