Passed
Push — master ( f5be59...b5f5af )
by Greg
06:01
created

Tree   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 682
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 259
dl 0
loc 682
rs 3.36
c 3
b 0
f 0
wmc 63

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 26 4
A rowMapper() 0 4 1
A setUserPreference() 0 19 2
B getNewXref() 0 33 7
A getPreference() 0 10 2
A getFactPrivacy() 0 3 1
A delete() 0 22 2
A exportGedcom() 0 42 3
B significantIndividual() 0 28 8
A canAcceptChanges() 0 3 1
B importGedcomFile() 0 37 7
A createRecord() 0 31 3
A id() 0 3 1
A getIndividualPrivacy() 0 3 1
A createIndividual() 0 31 3
A createFamily() 0 31 3
A createMediaObject() 0 31 3
A mediaFilesystem() 0 6 1
A getIndividualFactPrivacy() 0 3 1
A name() 0 3 1
A setPreference() 0 16 2
A deleteGenealogyData() 0 21 2
A hasPendingEdit() 0 6 1
A title() 0 3 1
A getUserPreference() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like Tree often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Tree, and based on these observations, apply Extract Interface, too.

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