Passed
Push — master ( 9f5cc2...6e9127 )
by Greg
05:50
created

Tree::__construct()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 26
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 18
nc 4
nop 3
dl 0
loc 26
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 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;
21
22
use Closure;
23
use Fisharebest\Flysystem\Adapter\ChrootAdapter;
24
use Fisharebest\Webtrees\Contracts\UserInterface;
25
use Fisharebest\Webtrees\Functions\FunctionsExport;
26
use Fisharebest\Webtrees\Services\PendingChangesService;
27
use Illuminate\Database\Capsule\Manager as DB;
28
use Illuminate\Database\Query\Expression;
29
use Illuminate\Support\Collection;
30
use Illuminate\Support\Str;
31
use InvalidArgumentException;
32
use League\Flysystem\Filesystem;
33
use League\Flysystem\FilesystemInterface;
34
use Psr\Http\Message\StreamInterface;
35
use stdClass;
36
37
use function app;
38
39
/**
40
 * Provide an interface to the wt_gedcom table.
41
 */
42
class Tree
43
{
44
    private const RESN_PRIVACY = [
45
        'none'         => Auth::PRIV_PRIVATE,
46
        'privacy'      => Auth::PRIV_USER,
47
        'confidential' => Auth::PRIV_NONE,
48
        'hidden'       => Auth::PRIV_HIDE,
49
    ];
50
51
    /** @var int The tree's ID number */
52
    private $id;
53
54
    /** @var string The tree's name */
55
    private $name;
56
57
    /** @var string The tree's title */
58
    private $title;
59
60
    /** @var int[] Default access rules for facts in this tree */
61
    private $fact_privacy;
62
63
    /** @var int[] Default access rules for individuals in this tree */
64
    private $individual_privacy;
65
66
    /** @var integer[][] Default access rules for individual facts in this tree */
67
    private $individual_fact_privacy;
68
69
    /** @var string[] Cached copy of the wt_gedcom_setting table. */
70
    private $preferences = [];
71
72
    /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */
73
    private $user_preferences = [];
74
75
    /**
76
     * Create a tree object.
77
     *
78
     * @param int    $id
79
     * @param string $name
80
     * @param string $title
81
     */
82
    public function __construct(int $id, string $name, string $title)
83
    {
84
        $this->id                      = $id;
85
        $this->name                    = $name;
86
        $this->title                   = $title;
87
        $this->fact_privacy            = [];
88
        $this->individual_privacy      = [];
89
        $this->individual_fact_privacy = [];
90
91
        // Load the privacy settings for this tree
92
        $rows = DB::table('default_resn')
93
            ->where('gedcom_id', '=', $this->id)
94
            ->get();
95
96
        foreach ($rows as $row) {
97
            // Convert GEDCOM privacy restriction to a webtrees access level.
98
            $row->resn = self::RESN_PRIVACY[$row->resn];
99
100
            if ($row->xref !== null) {
101
                if ($row->tag_type !== null) {
102
                    $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn;
103
                } else {
104
                    $this->individual_privacy[$row->xref] = $row->resn;
105
                }
106
            } else {
107
                $this->fact_privacy[$row->tag_type] = $row->resn;
108
            }
109
        }
110
    }
111
112
    /**
113
     * A closure which will create a record from a database row.
114
     *
115
     * @return Closure
116
     */
117
    public static function rowMapper(): Closure
118
    {
119
        return static function (stdClass $row): Tree {
120
            return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title);
121
        };
122
    }
123
124
    /**
125
     * Set the tree’s configuration settings.
126
     *
127
     * @param string $setting_name
128
     * @param string $setting_value
129
     *
130
     * @return $this
131
     */
132
    public function setPreference(string $setting_name, string $setting_value): Tree
133
    {
134
        if ($setting_value !== $this->getPreference($setting_name)) {
135
            DB::table('gedcom_setting')->updateOrInsert([
136
                'gedcom_id'    => $this->id,
137
                'setting_name' => $setting_name,
138
            ], [
139
                'setting_value' => $setting_value,
140
            ]);
141
142
            $this->preferences[$setting_name] = $setting_value;
143
144
            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this);
145
        }
146
147
        return $this;
148
    }
149
150
    /**
151
     * Get the tree’s configuration settings.
152
     *
153
     * @param string $setting_name
154
     * @param string $default
155
     *
156
     * @return string
157
     */
158
    public function getPreference(string $setting_name, string $default = ''): string
159
    {
160
        if ($this->preferences === []) {
161
            $this->preferences = DB::table('gedcom_setting')
162
                ->where('gedcom_id', '=', $this->id)
163
                ->pluck('setting_value', 'setting_name')
164
                ->all();
165
        }
166
167
        return $this->preferences[$setting_name] ?? $default;
168
    }
169
170
    /**
171
     * The name of this tree
172
     *
173
     * @return string
174
     */
175
    public function name(): string
176
    {
177
        return $this->name;
178
    }
179
180
    /**
181
     * The title of this tree
182
     *
183
     * @return string
184
     */
185
    public function title(): string
186
    {
187
        return $this->title;
188
    }
189
190
    /**
191
     * The fact-level privacy for this tree.
192
     *
193
     * @return int[]
194
     */
195
    public function getFactPrivacy(): array
196
    {
197
        return $this->fact_privacy;
198
    }
199
200
    /**
201
     * The individual-level privacy for this tree.
202
     *
203
     * @return int[]
204
     */
205
    public function getIndividualPrivacy(): array
206
    {
207
        return $this->individual_privacy;
208
    }
209
210
    /**
211
     * The individual-fact-level privacy for this tree.
212
     *
213
     * @return int[][]
214
     */
215
    public function getIndividualFactPrivacy(): array
216
    {
217
        return $this->individual_fact_privacy;
218
    }
219
220
    /**
221
     * Set the tree’s user-configuration settings.
222
     *
223
     * @param UserInterface $user
224
     * @param string        $setting_name
225
     * @param string        $setting_value
226
     *
227
     * @return $this
228
     */
229
    public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree
230
    {
231
        if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
232
            // Update the database
233
            DB::table('user_gedcom_setting')->updateOrInsert([
234
                'gedcom_id'    => $this->id(),
235
                'user_id'      => $user->id(),
236
                'setting_name' => $setting_name,
237
            ], [
238
                'setting_value' => $setting_value,
239
            ]);
240
241
            // Update the cache
242
            $this->user_preferences[$user->id()][$setting_name] = $setting_value;
243
            // Audit log of changes
244
            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this);
245
        }
246
247
        return $this;
248
    }
249
250
    /**
251
     * Get the tree’s user-configuration settings.
252
     *
253
     * @param UserInterface $user
254
     * @param string        $setting_name
255
     * @param string        $default
256
     *
257
     * @return string
258
     */
259
    public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string
260
    {
261
        // There are lots of settings, and we need to fetch lots of them on every page
262
        // so it is quicker to fetch them all in one go.
263
        if (!array_key_exists($user->id(), $this->user_preferences)) {
264
            $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting')
265
                ->where('user_id', '=', $user->id())
266
                ->where('gedcom_id', '=', $this->id)
267
                ->pluck('setting_value', 'setting_name')
268
                ->all();
269
        }
270
271
        return $this->user_preferences[$user->id()][$setting_name] ?? $default;
272
    }
273
274
    /**
275
     * The ID of this tree
276
     *
277
     * @return int
278
     */
279
    public function id(): int
280
    {
281
        return $this->id;
282
    }
283
284
    /**
285
     * Can a user accept changes for this tree?
286
     *
287
     * @param UserInterface $user
288
     *
289
     * @return bool
290
     */
291
    public function canAcceptChanges(UserInterface $user): bool
292
    {
293
        return Auth::isModerator($this, $user);
294
    }
295
296
    /**
297
     * Are there any pending edits for this tree, than need reviewing by a moderator.
298
     *
299
     * @return bool
300
     */
301
    public function hasPendingEdit(): bool
302
    {
303
        return DB::table('change')
304
            ->where('gedcom_id', '=', $this->id)
305
            ->where('status', '=', 'pending')
306
            ->exists();
307
    }
308
309
    /**
310
     * Delete everything relating to a tree
311
     *
312
     * @return void
313
     */
314
    public function delete(): void
315
    {
316
        // If this is the default tree, then unset it
317
        if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) {
318
            Site::setPreference('DEFAULT_GEDCOM', '');
319
        }
320
321
        $this->deleteGenealogyData(false);
322
323
        DB::table('block_setting')
324
            ->join('block', 'block.block_id', '=', 'block_setting.block_id')
325
            ->where('gedcom_id', '=', $this->id)
326
            ->delete();
327
        DB::table('block')->where('gedcom_id', '=', $this->id)->delete();
328
        DB::table('user_gedcom_setting')->where('gedcom_id', '=', $this->id)->delete();
329
        DB::table('gedcom_setting')->where('gedcom_id', '=', $this->id)->delete();
330
        DB::table('module_privacy')->where('gedcom_id', '=', $this->id)->delete();
331
        DB::table('hit_counter')->where('gedcom_id', '=', $this->id)->delete();
332
        DB::table('default_resn')->where('gedcom_id', '=', $this->id)->delete();
333
        DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete();
334
        DB::table('log')->where('gedcom_id', '=', $this->id)->delete();
335
        DB::table('gedcom')->where('gedcom_id', '=', $this->id)->delete();
336
    }
337
338
    /**
339
     * Delete all the genealogy data from a tree - in preparation for importing
340
     * new data. Optionally retain the media data, for when the user has been
341
     * editing their data offline using an application which deletes (or does not
342
     * support) media data.
343
     *
344
     * @param bool $keep_media
345
     *
346
     * @return void
347
     */
348
    public function deleteGenealogyData(bool $keep_media): void
349
    {
350
        DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete();
351
        DB::table('individuals')->where('i_file', '=', $this->id)->delete();
352
        DB::table('families')->where('f_file', '=', $this->id)->delete();
353
        DB::table('sources')->where('s_file', '=', $this->id)->delete();
354
        DB::table('other')->where('o_file', '=', $this->id)->delete();
355
        DB::table('places')->where('p_file', '=', $this->id)->delete();
356
        DB::table('placelinks')->where('pl_file', '=', $this->id)->delete();
357
        DB::table('name')->where('n_file', '=', $this->id)->delete();
358
        DB::table('dates')->where('d_file', '=', $this->id)->delete();
359
        DB::table('change')->where('gedcom_id', '=', $this->id)->delete();
360
361
        if ($keep_media) {
362
            DB::table('link')->where('l_file', '=', $this->id)
363
                ->where('l_type', '<>', 'OBJE')
364
                ->delete();
365
        } else {
366
            DB::table('link')->where('l_file', '=', $this->id)->delete();
367
            DB::table('media_file')->where('m_file', '=', $this->id)->delete();
368
            DB::table('media')->where('m_file', '=', $this->id)->delete();
369
        }
370
    }
371
372
    /**
373
     * Export the tree to a GEDCOM file
374
     *
375
     * @param resource $stream
376
     *
377
     * @return void
378
     */
379
    public function exportGedcom($stream): void
380
    {
381
        $buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this, 'UTF-8'));
382
383
        $union_families = DB::table('families')
384
            ->where('f_file', '=', $this->id)
385
            ->select(['f_gedcom AS gedcom', 'f_id AS xref', new Expression('LENGTH(f_id) AS len'), new Expression('2 AS n')]);
386
387
        $union_sources = DB::table('sources')
388
            ->where('s_file', '=', $this->id)
389
            ->select(['s_gedcom AS gedcom', 's_id AS xref', new Expression('LENGTH(s_id) AS len'), new Expression('3 AS n')]);
390
391
        $union_other = DB::table('other')
392
            ->where('o_file', '=', $this->id)
393
            ->whereNotIn('o_type', ['HEAD', 'TRLR'])
394
            ->select(['o_gedcom AS gedcom', 'o_id AS xref', new Expression('LENGTH(o_id) AS len'), new Expression('4 AS n')]);
395
396
        $union_media = DB::table('media')
397
            ->where('m_file', '=', $this->id)
398
            ->select(['m_gedcom AS gedcom', 'm_id AS xref', new Expression('LENGTH(m_id) AS len'), new Expression('5 AS n')]);
399
400
        DB::table('individuals')
401
            ->where('i_file', '=', $this->id)
402
            ->select(['i_gedcom AS gedcom', 'i_id AS xref', new Expression('LENGTH(i_id) AS len'), new Expression('1 AS n')])
403
            ->union($union_families)
404
            ->union($union_sources)
405
            ->union($union_other)
406
            ->union($union_media)
407
            ->orderBy('n')
408
            ->orderBy('len')
409
            ->orderBy('xref')
410
            ->chunk(1000, static function (Collection $rows) use ($stream, &$buffer): void {
411
                foreach ($rows as $row) {
412
                    $buffer .= FunctionsExport::reformatRecord($row->gedcom);
413
                    if (strlen($buffer) > 65535) {
414
                        fwrite($stream, $buffer);
415
                        $buffer = '';
416
                    }
417
                }
418
            });
419
420
        fwrite($stream, $buffer . '0 TRLR' . Gedcom::EOL);
421
    }
422
423
    /**
424
     * Import data from a gedcom file into this tree.
425
     *
426
     * @param StreamInterface $stream   The GEDCOM file.
427
     * @param string          $filename The preferred filename, for export/download.
428
     *
429
     * @return void
430
     */
431
    public function importGedcomFile(StreamInterface $stream, string $filename): void
432
    {
433
        // Read the file in blocks of roughly 64K. Ensure that each block
434
        // contains complete gedcom records. This will ensure we don’t split
435
        // multi-byte characters, as well as simplifying the code to import
436
        // each block.
437
438
        $file_data = '';
439
440
        $this->deleteGenealogyData((bool) $this->getPreference('keep_media'));
441
        $this->setPreference('gedcom_filename', $filename);
442
        $this->setPreference('imported', '0');
443
444
        while (!$stream->eof()) {
445
            $file_data .= $stream->read(65536);
446
            // There is no strrpos() function that searches for substrings :-(
447
            for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) {
448
                if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) {
449
                    // We’ve found the last record boundary in this chunk of data
450
                    break;
451
                }
452
            }
453
            if ($pos) {
454
                DB::table('gedcom_chunk')->insert([
455
                    'gedcom_id'  => $this->id,
456
                    'chunk_data' => substr($file_data, 0, $pos),
457
                ]);
458
459
                $file_data = substr($file_data, $pos);
460
            }
461
        }
462
        DB::table('gedcom_chunk')->insert([
463
            'gedcom_id'  => $this->id,
464
            'chunk_data' => $file_data,
465
        ]);
466
467
        $stream->close();
468
    }
469
470
    /**
471
     * Create a new record from GEDCOM data.
472
     *
473
     * @param string $gedcom
474
     *
475
     * @return GedcomRecord|Individual|Family|Note|Source|Repository|Media
476
     * @throws InvalidArgumentException
477
     */
478
    public function createRecord(string $gedcom): GedcomRecord
479
    {
480
        if (!Str::startsWith($gedcom, '0 @@ ')) {
481
            throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@');
482
        }
483
484
        $xref   = $this->getNewXref();
485
        $gedcom = '0 @' . $xref . '@ ' . Str::after($gedcom, '0 @@ ');
486
487
        // Create a change record
488
        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName();
489
490
        // Create a pending change
491
        DB::table('change')->insert([
492
            'gedcom_id'  => $this->id,
493
            'xref'       => $xref,
494
            'old_gedcom' => '',
495
            'new_gedcom' => $gedcom,
496
            'user_id'    => Auth::id(),
497
        ]);
498
499
        // Accept this pending change
500
        if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS)) {
501
            $record = new GedcomRecord($xref, $gedcom, null, $this);
502
            
503
            app(PendingChangesService::class)->acceptRecord($record);
504
505
            return $record;
506
        }
507
508
        return GedcomRecord::getInstance($xref, $this, $gedcom);
509
    }
510
511
    /**
512
     * Generate a new XREF, unique across all family trees
513
     *
514
     * @return string
515
     */
516
    public function getNewXref(): string
517
    {
518
        // Lock the row, so that only one new XREF may be generated at a time.
519
        DB::table('site_setting')
520
            ->where('setting_name', '=', 'next_xref')
521
            ->lockForUpdate()
522
            ->get();
523
524
        $prefix = 'X';
525
526
        $increment = 1.0;
527
        do {
528
            $num = (int) Site::getPreference('next_xref') + (int) $increment;
529
530
            // This exponential increment allows us to scan over large blocks of
531
            // existing data in a reasonable time.
532
            $increment *= 1.01;
533
534
            $xref = $prefix . $num;
535
536
            // Records may already exist with this sequence number.
537
            $already_used =
538
                DB::table('individuals')->where('i_id', '=', $xref)->exists() ||
539
                DB::table('families')->where('f_id', '=', $xref)->exists() ||
540
                DB::table('sources')->where('s_id', '=', $xref)->exists() ||
541
                DB::table('media')->where('m_id', '=', $xref)->exists() ||
542
                DB::table('other')->where('o_id', '=', $xref)->exists() ||
543
                DB::table('change')->where('xref', '=', $xref)->exists();
544
        } while ($already_used);
545
546
        Site::setPreference('next_xref', (string) $num);
547
548
        return $xref;
549
    }
550
551
    /**
552
     * Create a new family from GEDCOM data.
553
     *
554
     * @param string $gedcom
555
     *
556
     * @return Family
557
     * @throws InvalidArgumentException
558
     */
559
    public function createFamily(string $gedcom): GedcomRecord
560
    {
561
        if (!Str::startsWith($gedcom, '0 @@ FAM')) {
562
            throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM');
563
        }
564
565
        $xref   = $this->getNewXref();
566
        $gedcom = '0 @' . $xref . '@ FAM' . Str::after($gedcom, '0 @@ FAM');
567
568
        // Create a change record
569
        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName();
570
571
        // Create a pending change
572
        DB::table('change')->insert([
573
            'gedcom_id'  => $this->id,
574
            'xref'       => $xref,
575
            'old_gedcom' => '',
576
            'new_gedcom' => $gedcom,
577
            'user_id'    => Auth::id(),
578
        ]);
579
580
        // Accept this pending change
581
        if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') {
582
            $record = new Family($xref, $gedcom, null, $this);
583
584
            app(PendingChangesService::class)->acceptRecord($record);
585
586
            return $record;
587
        }
588
589
        return new Family($xref, '', $gedcom, $this);
590
    }
591
592
    /**
593
     * Create a new individual from GEDCOM data.
594
     *
595
     * @param string $gedcom
596
     *
597
     * @return Individual
598
     * @throws InvalidArgumentException
599
     */
600
    public function createIndividual(string $gedcom): GedcomRecord
601
    {
602
        if (!Str::startsWith($gedcom, '0 @@ INDI')) {
603
            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI');
604
        }
605
606
        $xref   = $this->getNewXref();
607
        $gedcom = '0 @' . $xref . '@ INDI' . Str::after($gedcom, '0 @@ INDI');
608
609
        // Create a change record
610
        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName();
611
612
        // Create a pending change
613
        DB::table('change')->insert([
614
            'gedcom_id'  => $this->id,
615
            'xref'       => $xref,
616
            'old_gedcom' => '',
617
            'new_gedcom' => $gedcom,
618
            'user_id'    => Auth::id(),
619
        ]);
620
621
        // Accept this pending change
622
        if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') {
623
            $record = new Individual($xref, $gedcom, null, $this);
624
625
            app(PendingChangesService::class)->acceptRecord($record);
626
627
            return $record;
628
        }
629
630
        return new Individual($xref, '', $gedcom, $this);
631
    }
632
633
    /**
634
     * Create a new media object from GEDCOM data.
635
     *
636
     * @param string $gedcom
637
     *
638
     * @return Media
639
     * @throws InvalidArgumentException
640
     */
641
    public function createMediaObject(string $gedcom): Media
642
    {
643
        if (!Str::startsWith($gedcom, '0 @@ OBJE')) {
644
            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE');
645
        }
646
647
        $xref   = $this->getNewXref();
648
        $gedcom = '0 @' . $xref . '@ OBJE' . Str::after($gedcom, '0 @@ OBJE');
649
650
        // Create a change record
651
        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName();
652
653
        // Create a pending change
654
        DB::table('change')->insert([
655
            'gedcom_id'  => $this->id,
656
            'xref'       => $xref,
657
            'old_gedcom' => '',
658
            'new_gedcom' => $gedcom,
659
            'user_id'    => Auth::id(),
660
        ]);
661
662
        // Accept this pending change
663
        if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') {
664
            $record = new Media($xref, $gedcom, null, $this);
665
666
            app(PendingChangesService::class)->acceptRecord($record);
667
668
            return $record;
669
        }
670
671
        return new Media($xref, '', $gedcom, $this);
672
    }
673
674
    /**
675
     * What is the most significant individual in this tree.
676
     *
677
     * @param UserInterface $user
678
     * @param string        $xref
679
     *
680
     * @return Individual
681
     */
682
    public function significantIndividual(UserInterface $user, $xref = ''): Individual
683
    {
684
        if ($xref === '') {
685
            $individual = null;
686
        } else {
687
            $individual = Individual::getInstance($xref, $this);
688
689
            if ($individual === null) {
690
                $family = Family::getInstance($xref, $this);
691
692
                if ($family instanceof Family) {
693
                    $individual = $family->spouses()->first() ?? $family->children()->first();
694
                }
695
            }
696
        }
697
698
        if ($individual === null && $this->getUserPreference($user, User::PREF_TREE_DEFAULT_XREF) !== '') {
699
            $individual = Individual::getInstance($this->getUserPreference($user, User::PREF_TREE_DEFAULT_XREF), $this);
700
        }
701
702
        if ($individual === null && $this->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF) !== '') {
703
            $individual = Individual::getInstance($this->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF), $this);
704
        }
705
706
        if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') {
707
            $individual = Individual::getInstance($this->getPreference('PEDIGREE_ROOT_ID'), $this);
708
        }
709
        if ($individual === null) {
710
            $xref = (string) DB::table('individuals')
711
                ->where('i_file', '=', $this->id())
712
                ->min('i_id');
713
714
            $individual = Individual::getInstance($xref, $this);
715
        }
716
        if ($individual === null) {
717
            // always return a record
718
            $individual = new Individual('I', '0 @I@ INDI', null, $this);
719
        }
720
721
        return $individual;
722
    }
723
724
    /**
725
     * Where do we store our media files.
726
     *
727
     * @param FilesystemInterface $data_filesystem
728
     *
729
     * @return FilesystemInterface
730
     */
731
    public function mediaFilesystem(FilesystemInterface $data_filesystem): FilesystemInterface
732
    {
733
        $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/');
734
        $adapter   = new ChrootAdapter($data_filesystem, $media_dir);
735
736
        return new Filesystem($adapter);
737
    }
738
}
739