Passed
Push — master ( d09618...1b7bac )
by Greg
05:28
created

FunctionsPrintFacts::printMainMedia()   C

Complexity

Conditions 16
Paths 90

Size

Total Lines 72
Code Lines 58

Duplication

Lines 0
Ratio 0 %

Importance

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