Passed
Push — master ( e25212...5afbc5 )
by Greg
05:14
created

Tree::getNameList()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 8
rs 10
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
declare(strict_types=1);
18
19
namespace Fisharebest\Webtrees;
20
21
use Closure;
22
use Fisharebest\Flysystem\Adapter\ChrootAdapter;
23
use Fisharebest\Webtrees\Contracts\UserInterface;
24
use Fisharebest\Webtrees\Functions\FunctionsExport;
25
use Fisharebest\Webtrees\Functions\FunctionsImport;
26
use Fisharebest\Webtrees\Services\TreeService;
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
/**
38
 * Provide an interface to the wt_gedcom table.
39
 */
40
class Tree
41
{
42
    private const RESN_PRIVACY = [
43
        'none'         => Auth::PRIV_PRIVATE,
44
        'privacy'      => Auth::PRIV_USER,
45
        'confidential' => Auth::PRIV_NONE,
46
        'hidden'       => Auth::PRIV_HIDE,
47
    ];
48
    /** @var Tree[] All trees that we have permission to see, indexed by ID. */
49
    public static $trees = [];
50
    /** @var int The tree's ID number */
51
    private $id;
52
    /** @var string The tree's name */
53
    private $name;
54
    /** @var string The tree's title */
55
    private $title;
56
    /** @var int[] Default access rules for facts in this tree */
57
    private $fact_privacy;
58
    /** @var int[] Default access rules for individuals in this tree */
59
    private $individual_privacy;
60
    /** @var integer[][] Default access rules for individual facts in this tree */
61
    private $individual_fact_privacy;
62
    /** @var string[] Cached copy of the wt_gedcom_setting table. */
63
    private $preferences = [];
64
    /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */
65
    private $user_preferences = [];
66
67
    /**
68
     * Create a tree object. This is a private constructor - it can only
69
     * be called from Tree::getAll() to ensure proper initialisation.
70
     *
71
     * @param int    $id
72
     * @param string $name
73
     * @param string $title
74
     */
75
    public function __construct(int $id, string $name, string $title)
76
    {
77
        $this->id                      = $id;
78
        $this->name                    = $name;
79
        $this->title                   = $title;
80
        $this->fact_privacy            = [];
81
        $this->individual_privacy      = [];
82
        $this->individual_fact_privacy = [];
83
84
        // Load the privacy settings for this tree
85
        $rows = DB::table('default_resn')
86
            ->where('gedcom_id', '=', $this->id)
87
            ->get();
88
89
        foreach ($rows as $row) {
90
            // Convert GEDCOM privacy restriction to a webtrees access level.
91
            $row->resn = self::RESN_PRIVACY[$row->resn];
92
93
            if ($row->xref !== null) {
94
                if ($row->tag_type !== null) {
95
                    $this->individual_fact_privacy[$row->xref][$row->tag_type] = (int) $row->resn;
96
                } else {
97
                    $this->individual_privacy[$row->xref] = (int) $row->resn;
98
                }
99
            } else {
100
                $this->fact_privacy[$row->tag_type] = (int) $row->resn;
101
            }
102
        }
103
    }
104
105
    /**
106
     * A closure which will create a record from a database row.
107
     *
108
     * @return Closure
109
     */
110
    public static function rowMapper(): Closure
111
    {
112
        return static function (stdClass $row): Tree {
113
            return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title);
114
        };
115
    }
116
117
    /**
118
     * Find the tree with a specific ID.
119
     *
120
     * @param int $tree_id
121
     *
122
     * @return Tree
123
     */
124
    public static function findById(int $tree_id): Tree
125
    {
126
        return self::getAll()[$tree_id];
0 ignored issues
show
Deprecated Code introduced by
The function Fisharebest\Webtrees\Tree::getAll() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

126
        return /** @scrutinizer ignore-deprecated */ self::getAll()[$tree_id];
Loading history...
127
    }
128
129
    /**
130
     * Fetch all the trees that we have permission to access.
131
     *
132
     * @return Tree[]
133
     * @deprecated
134
     */
135
    public static function getAll(): array
136
    {
137
        if (empty(self::$trees)) {
138
            self::$trees = self::all()->all();
0 ignored issues
show
Deprecated Code introduced by
The function Fisharebest\Webtrees\Tree::all() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

138
            self::$trees = /** @scrutinizer ignore-deprecated */ self::all()->all();
Loading history...
139
        }
140
141
        return self::$trees;
142
    }
143
144
    /**
145
     * All the trees that we have permission to access.
146
     *
147
     * @return Collection
148
     * @deprecated
149
     */
150
    public static function all(): Collection
151
    {
152
        return (new TreeService())->all();
153
    }
154
155
    /**
156
     * Create arguments to select_edit_control()
157
     * Note - these will be escaped later
158
     *
159
     * @return string[]
160
     */
161
    public static function getIdList(): array
162
    {
163
        $list = [];
164
        foreach (self::getAll() as $tree) {
0 ignored issues
show
Deprecated Code introduced by
The function Fisharebest\Webtrees\Tree::getAll() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

164
        foreach (/** @scrutinizer ignore-deprecated */ self::getAll() as $tree) {
Loading history...
165
            $list[$tree->id] = $tree->title;
166
        }
167
168
        return $list;
169
    }
170
171
    /**
172
     * Create arguments to select_edit_control()
173
     * Note - these will be escaped later
174
     *
175
     * @return string[]
176
     */
177
    public static function getNameList(): array
178
    {
179
        $list = [];
180
        foreach (self::getAll() as $tree) {
0 ignored issues
show
Deprecated Code introduced by
The function Fisharebest\Webtrees\Tree::getAll() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

180
        foreach (/** @scrutinizer ignore-deprecated */ self::getAll() as $tree) {
Loading history...
181
            $list[$tree->name] = $tree->title;
182
        }
183
184
        return $list;
185
    }
186
187
    /**
188
     * Create a new tree
189
     *
190
     * @param string $tree_name
191
     * @param string $tree_title
192
     *
193
     * @return Tree
194
     * @deprecated
195
     */
196
    public static function create(string $tree_name, string $tree_title): Tree
197
    {
198
        return (new TreeService())->create($tree_name, $tree_title);
199
    }
200
201
    /**
202
     * Find the tree with a specific name.
203
     *
204
     * @param string $name
205
     *
206
     * @return Tree|null
207
     * @deprecated
208
     */
209
    public static function findByName($name): ?Tree
210
    {
211
        foreach (self::getAll() as $tree) {
0 ignored issues
show
Deprecated Code introduced by
The function Fisharebest\Webtrees\Tree::getAll() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

211
        foreach (/** @scrutinizer ignore-deprecated */ self::getAll() as $tree) {
Loading history...
212
            if ($tree->name === $name) {
213
                return $tree;
214
            }
215
        }
216
217
        return null;
218
    }
219
220
    /**
221
     * Set the tree’s configuration settings.
222
     *
223
     * @param string $setting_name
224
     * @param string $setting_value
225
     *
226
     * @return $this
227
     */
228
    public function setPreference(string $setting_name, string $setting_value): Tree
229
    {
230
        if ($setting_value !== $this->getPreference($setting_name)) {
231
            DB::table('gedcom_setting')->updateOrInsert([
232
                'gedcom_id'    => $this->id,
233
                'setting_name' => $setting_name,
234
            ], [
235
                'setting_value' => $setting_value,
236
            ]);
237
238
            $this->preferences[$setting_name] = $setting_value;
239
240
            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this);
241
        }
242
243
        return $this;
244
    }
245
246
    /**
247
     * Get the tree’s configuration settings.
248
     *
249
     * @param string $setting_name
250
     * @param string $default
251
     *
252
     * @return string
253
     */
254
    public function getPreference(string $setting_name, string $default = ''): string
255
    {
256
        if (empty($this->preferences)) {
257
            $this->preferences = DB::table('gedcom_setting')
258
                ->where('gedcom_id', '=', $this->id)
259
                ->pluck('setting_value', 'setting_name')
260
                ->all();
261
        }
262
263
        return $this->preferences[$setting_name] ?? $default;
264
    }
265
266
    /**
267
     * The name of this tree
268
     *
269
     * @return string
270
     */
271
    public function name(): string
272
    {
273
        return $this->name;
274
    }
275
276
    /**
277
     * The title of this tree
278
     *
279
     * @return string
280
     */
281
    public function title(): string
282
    {
283
        return $this->title;
284
    }
285
286
    /**
287
     * The fact-level privacy for this tree.
288
     *
289
     * @return int[]
290
     */
291
    public function getFactPrivacy(): array
292
    {
293
        return $this->fact_privacy;
294
    }
295
296
    /**
297
     * The individual-level privacy for this tree.
298
     *
299
     * @return int[]
300
     */
301
    public function getIndividualPrivacy(): array
302
    {
303
        return $this->individual_privacy;
304
    }
305
306
    /**
307
     * The individual-fact-level privacy for this tree.
308
     *
309
     * @return int[][]
310
     */
311
    public function getIndividualFactPrivacy(): array
312
    {
313
        return $this->individual_fact_privacy;
314
    }
315
316
    /**
317
     * Set the tree’s user-configuration settings.
318
     *
319
     * @param UserInterface $user
320
     * @param string        $setting_name
321
     * @param string        $setting_value
322
     *
323
     * @return $this
324
     */
325
    public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree
326
    {
327
        if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
328
            // Update the database
329
            DB::table('user_gedcom_setting')->updateOrInsert([
330
                'gedcom_id'    => $this->id(),
331
                'user_id'      => $user->id(),
332
                'setting_name' => $setting_name,
333
            ], [
334
                'setting_value' => $setting_value,
335
            ]);
336
337
            // Update the cache
338
            $this->user_preferences[$user->id()][$setting_name] = $setting_value;
339
            // Audit log of changes
340
            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this);
341
        }
342
343
        return $this;
344
    }
345
346
    /**
347
     * Get the tree’s user-configuration settings.
348
     *
349
     * @param UserInterface $user
350
     * @param string        $setting_name
351
     * @param string        $default
352
     *
353
     * @return string
354
     */
355
    public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string
356
    {
357
        // There are lots of settings, and we need to fetch lots of them on every page
358
        // so it is quicker to fetch them all in one go.
359
        if (!array_key_exists($user->id(), $this->user_preferences)) {
360
            $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting')
361
                ->where('user_id', '=', $user->id())
362
                ->where('gedcom_id', '=', $this->id)
363
                ->pluck('setting_value', 'setting_name')
364
                ->all();
365
        }
366
367
        return $this->user_preferences[$user->id()][$setting_name] ?? $default;
368
    }
369
370
    /**
371
     * The ID of this tree
372
     *
373
     * @return int
374
     */
375
    public function id(): int
376
    {
377
        return $this->id;
378
    }
379
380
    /**
381
     * Can a user accept changes for this tree?
382
     *
383
     * @param UserInterface $user
384
     *
385
     * @return bool
386
     */
387
    public function canAcceptChanges(UserInterface $user): bool
388
    {
389
        return Auth::isModerator($this, $user);
390
    }
391
392
    /**
393
     * Are there any pending edits for this tree, than need reviewing by a moderator.
394
     *
395
     * @return bool
396
     */
397
    public function hasPendingEdit(): bool
398
    {
399
        return DB::table('change')
400
            ->where('gedcom_id', '=', $this->id)
401
            ->where('status', '=', 'pending')
402
            ->exists();
403
    }
404
405
    /**
406
     * Delete everything relating to a tree
407
     *
408
     * @return void
409
     */
410
    public function delete(): void
411
    {
412
        // If this is the default tree, then unset it
413
        if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) {
414
            Site::setPreference('DEFAULT_GEDCOM', '');
415
        }
416
417
        $this->deleteGenealogyData(false);
418
419
        DB::table('block_setting')
420
            ->join('block', 'block.block_id', '=', 'block_setting.block_id')
421
            ->where('gedcom_id', '=', $this->id)
422
            ->delete();
423
        DB::table('block')->where('gedcom_id', '=', $this->id)->delete();
424
        DB::table('user_gedcom_setting')->where('gedcom_id', '=', $this->id)->delete();
425
        DB::table('gedcom_setting')->where('gedcom_id', '=', $this->id)->delete();
426
        DB::table('module_privacy')->where('gedcom_id', '=', $this->id)->delete();
427
        DB::table('hit_counter')->where('gedcom_id', '=', $this->id)->delete();
428
        DB::table('default_resn')->where('gedcom_id', '=', $this->id)->delete();
429
        DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete();
430
        DB::table('log')->where('gedcom_id', '=', $this->id)->delete();
431
        DB::table('gedcom')->where('gedcom_id', '=', $this->id)->delete();
432
433
        // After updating the database, we need to fetch a new (sorted) copy
434
        self::$trees = [];
435
    }
436
437
    /**
438
     * Delete all the genealogy data from a tree - in preparation for importing
439
     * new data. Optionally retain the media data, for when the user has been
440
     * editing their data offline using an application which deletes (or does not
441
     * support) media data.
442
     *
443
     * @param bool $keep_media
444
     *
445
     * @return void
446
     */
447
    public function deleteGenealogyData(bool $keep_media): void
448
    {
449
        DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete();
450
        DB::table('individuals')->where('i_file', '=', $this->id)->delete();
451
        DB::table('families')->where('f_file', '=', $this->id)->delete();
452
        DB::table('sources')->where('s_file', '=', $this->id)->delete();
453
        DB::table('other')->where('o_file', '=', $this->id)->delete();
454
        DB::table('places')->where('p_file', '=', $this->id)->delete();
455
        DB::table('placelinks')->where('pl_file', '=', $this->id)->delete();
456
        DB::table('name')->where('n_file', '=', $this->id)->delete();
457
        DB::table('dates')->where('d_file', '=', $this->id)->delete();
458
        DB::table('change')->where('gedcom_id', '=', $this->id)->delete();
459
460
        if ($keep_media) {
461
            DB::table('link')->where('l_file', '=', $this->id)
462
                ->where('l_type', '<>', 'OBJE')
463
                ->delete();
464
        } else {
465
            DB::table('link')->where('l_file', '=', $this->id)->delete();
466
            DB::table('media_file')->where('m_file', '=', $this->id)->delete();
467
            DB::table('media')->where('m_file', '=', $this->id)->delete();
468
        }
469
    }
470
471
    /**
472
     * Export the tree to a GEDCOM file
473
     *
474
     * @param resource $stream
475
     *
476
     * @return void
477
     */
478
    public function exportGedcom($stream): void
479
    {
480
        $buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this, 'UTF-8'));
481
482
        $union_families = DB::table('families')
483
            ->where('f_file', '=', $this->id)
484
            ->select(['f_gedcom AS gedcom', 'f_id AS xref', new Expression('LENGTH(f_id) AS len'), new Expression('2 AS n')]);
485
486
        $union_sources = DB::table('sources')
487
            ->where('s_file', '=', $this->id)
488
            ->select(['s_gedcom AS gedcom', 's_id AS xref', new Expression('LENGTH(s_id) AS len'), new Expression('3 AS n')]);
489
490
        $union_other = DB::table('other')
491
            ->where('o_file', '=', $this->id)
492
            ->whereNotIn('o_type', ['HEAD', 'TRLR'])
493
            ->select(['o_gedcom AS gedcom', 'o_id AS xref', new Expression('LENGTH(o_id) AS len'), new Expression('4 AS n')]);
494
495
        $union_media = DB::table('media')
496
            ->where('m_file', '=', $this->id)
497
            ->select(['m_gedcom AS gedcom', 'm_id AS xref', new Expression('LENGTH(m_id) AS len'), new Expression('5 AS n')]);
498
499
        DB::table('individuals')
500
            ->where('i_file', '=', $this->id)
501
            ->select(['i_gedcom AS gedcom', 'i_id AS xref', new Expression('LENGTH(i_id) AS len'), new Expression('1 AS n')])
502
            ->union($union_families)
503
            ->union($union_sources)
504
            ->union($union_other)
505
            ->union($union_media)
506
            ->orderBy('n')
507
            ->orderBy('len')
508
            ->orderBy('xref')
509
            ->chunk(1000, static function (Collection $rows) use ($stream, &$buffer): void {
510
                foreach ($rows as $row) {
511
                    $buffer .= FunctionsExport::reformatRecord($row->gedcom);
512
                    if (strlen($buffer) > 65535) {
513
                        fwrite($stream, $buffer);
514
                        $buffer = '';
515
                    }
516
                }
517
            });
518
519
        fwrite($stream, $buffer . '0 TRLR' . Gedcom::EOL);
520
    }
521
522
    /**
523
     * Import data from a gedcom file into this tree.
524
     *
525
     * @param StreamInterface $stream   The GEDCOM file.
526
     * @param string          $filename The preferred filename, for export/download.
527
     *
528
     * @return void
529
     */
530
    public function importGedcomFile(StreamInterface $stream, string $filename): void
531
    {
532
        // Read the file in blocks of roughly 64K. Ensure that each block
533
        // contains complete gedcom records. This will ensure we don’t split
534
        // multi-byte characters, as well as simplifying the code to import
535
        // each block.
536
537
        $file_data = '';
538
539
        $this->deleteGenealogyData((bool) $this->getPreference('keep_media'));
540
        $this->setPreference('gedcom_filename', $filename);
541
        $this->setPreference('imported', '0');
542
543
        while (!$stream->eof()) {
544
            $file_data .= $stream->read(65536);
545
            // There is no strrpos() function that searches for substrings :-(
546
            for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) {
547
                if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) {
548
                    // We’ve found the last record boundary in this chunk of data
549
                    break;
550
                }
551
            }
552
            if ($pos) {
553
                DB::table('gedcom_chunk')->insert([
554
                    'gedcom_id'  => $this->id,
555
                    'chunk_data' => substr($file_data, 0, $pos),
556
                ]);
557
558
                $file_data = substr($file_data, $pos);
559
            }
560
        }
561
        DB::table('gedcom_chunk')->insert([
562
            'gedcom_id'  => $this->id,
563
            'chunk_data' => $file_data,
564
        ]);
565
566
        $stream->close();
567
    }
568
569
    /**
570
     * Create a new record from GEDCOM data.
571
     *
572
     * @param string $gedcom
573
     *
574
     * @return GedcomRecord|Individual|Family|Note|Source|Repository|Media
575
     * @throws InvalidArgumentException
576
     */
577
    public function createRecord(string $gedcom): GedcomRecord
578
    {
579
        if (!Str::startsWith($gedcom, '0 @@ ')) {
580
            throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@');
581
        }
582
583
        $xref   = $this->getNewXref();
584
        $gedcom = '0 @' . $xref . '@ ' . Str::after($gedcom, '0 @@ ');
585
586
        // Create a change record
587
        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName();
588
589
        // Create a pending change
590
        DB::table('change')->insert([
591
            'gedcom_id'  => $this->id,
592
            'xref'       => $xref,
593
            'old_gedcom' => '',
594
            'new_gedcom' => $gedcom,
595
            'user_id'    => Auth::id(),
596
        ]);
597
598
        // Accept this pending change
599
        if (Auth::user()->getPreference('auto_accept')) {
600
            FunctionsImport::acceptAllChanges($xref, $this);
601
602
            return new GedcomRecord($xref, $gedcom, null, $this);
603
        }
604
605
        return GedcomRecord::getInstance($xref, $this, $gedcom);
606
    }
607
608
    /**
609
     * Generate a new XREF, unique across all family trees
610
     *
611
     * @return string
612
     */
613
    public function getNewXref(): string
614
    {
615
        // Lock the row, so that only one new XREF may be generated at a time.
616
        DB::table('site_setting')
617
            ->where('setting_name', '=', 'next_xref')
618
            ->lockForUpdate()
619
            ->get();
620
621
        $prefix = 'X';
622
623
        $increment = 1.0;
624
        do {
625
            $num = (int) Site::getPreference('next_xref') + (int) $increment;
626
627
            // This exponential increment allows us to scan over large blocks of
628
            // existing data in a reasonable time.
629
            $increment *= 1.01;
630
631
            $xref = $prefix . $num;
632
633
            // Records may already exist with this sequence number.
634
            $already_used =
635
                DB::table('individuals')->where('i_id', '=', $xref)->exists() ||
636
                DB::table('families')->where('f_id', '=', $xref)->exists() ||
637
                DB::table('sources')->where('s_id', '=', $xref)->exists() ||
638
                DB::table('media')->where('m_id', '=', $xref)->exists() ||
639
                DB::table('other')->where('o_id', '=', $xref)->exists() ||
640
                DB::table('change')->where('xref', '=', $xref)->exists();
641
        } while ($already_used);
642
643
        Site::setPreference('next_xref', (string) $num);
644
645
        return $xref;
646
    }
647
648
    /**
649
     * Create a new family from GEDCOM data.
650
     *
651
     * @param string $gedcom
652
     *
653
     * @return Family
654
     * @throws InvalidArgumentException
655
     */
656
    public function createFamily(string $gedcom): GedcomRecord
657
    {
658
        if (!Str::startsWith($gedcom, '0 @@ FAM')) {
659
            throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM');
660
        }
661
662
        $xref   = $this->getNewXref();
663
        $gedcom = '0 @' . $xref . '@ FAM' . Str::after($gedcom, '0 @@ FAM');
664
665
        // Create a change record
666
        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName();
667
668
        // Create a pending change
669
        DB::table('change')->insert([
670
            'gedcom_id'  => $this->id,
671
            'xref'       => $xref,
672
            'old_gedcom' => '',
673
            'new_gedcom' => $gedcom,
674
            'user_id'    => Auth::id(),
675
        ]);
676
677
        // Accept this pending change
678
        if (Auth::user()->getPreference('auto_accept')) {
679
            FunctionsImport::acceptAllChanges($xref, $this);
680
681
            return new Family($xref, $gedcom, null, $this);
682
        }
683
684
        return new Family($xref, '', $gedcom, $this);
685
    }
686
687
    /**
688
     * Create a new individual from GEDCOM data.
689
     *
690
     * @param string $gedcom
691
     *
692
     * @return Individual
693
     * @throws InvalidArgumentException
694
     */
695
    public function createIndividual(string $gedcom): GedcomRecord
696
    {
697
        if (!Str::startsWith($gedcom, '0 @@ INDI')) {
698
            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI');
699
        }
700
701
        $xref   = $this->getNewXref();
702
        $gedcom = '0 @' . $xref . '@ INDI' . Str::after($gedcom, '0 @@ INDI');
703
704
        // Create a change record
705
        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName();
706
707
        // Create a pending change
708
        DB::table('change')->insert([
709
            'gedcom_id'  => $this->id,
710
            'xref'       => $xref,
711
            'old_gedcom' => '',
712
            'new_gedcom' => $gedcom,
713
            'user_id'    => Auth::id(),
714
        ]);
715
716
        // Accept this pending change
717
        if (Auth::user()->getPreference('auto_accept')) {
718
            FunctionsImport::acceptAllChanges($xref, $this);
719
720
            return new Individual($xref, $gedcom, null, $this);
721
        }
722
723
        return new Individual($xref, '', $gedcom, $this);
724
    }
725
726
    /**
727
     * Create a new media object from GEDCOM data.
728
     *
729
     * @param string $gedcom
730
     *
731
     * @return Media
732
     * @throws InvalidArgumentException
733
     */
734
    public function createMediaObject(string $gedcom): Media
735
    {
736
        if (!Str::startsWith($gedcom, '0 @@ OBJE')) {
737
            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE');
738
        }
739
740
        $xref   = $this->getNewXref();
741
        $gedcom = '0 @' . $xref . '@ OBJE' . Str::after($gedcom, '0 @@ OBJE');
742
743
        // Create a change record
744
        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName();
745
746
        // Create a pending change
747
        DB::table('change')->insert([
748
            'gedcom_id'  => $this->id,
749
            'xref'       => $xref,
750
            'old_gedcom' => '',
751
            'new_gedcom' => $gedcom,
752
            'user_id'    => Auth::id(),
753
        ]);
754
755
        // Accept this pending change
756
        if (Auth::user()->getPreference('auto_accept')) {
757
            FunctionsImport::acceptAllChanges($xref, $this);
758
759
            return new Media($xref, $gedcom, null, $this);
760
        }
761
762
        return new Media($xref, '', $gedcom, $this);
763
    }
764
765
    /**
766
     * What is the most significant individual in this tree.
767
     *
768
     * @param UserInterface $user
769
     *
770
     * @return Individual
771
     */
772
    public function significantIndividual(UserInterface $user): Individual
773
    {
774
        $individual = null;
775
776
        if ($this->getUserPreference($user, 'rootid') !== '') {
777
            $individual = Individual::getInstance($this->getUserPreference($user, 'rootid'), $this);
778
        }
779
780
        if ($individual === null && $this->getUserPreference($user, 'gedcomid') !== '') {
781
            $individual = Individual::getInstance($this->getUserPreference($user, 'gedcomid'), $this);
782
        }
783
784
        if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') {
785
            $individual = Individual::getInstance($this->getPreference('PEDIGREE_ROOT_ID'), $this);
786
        }
787
        if ($individual === null) {
788
            $xref = (string) DB::table('individuals')
789
                ->where('i_file', '=', $this->id())
790
                ->min('i_id');
791
792
            $individual = Individual::getInstance($xref, $this);
793
        }
794
        if ($individual === null) {
795
            // always return a record
796
            $individual = new Individual('I', '0 @I@ INDI', null, $this);
797
        }
798
799
        return $individual;
800
    }
801
802
    /**
803
     * Where do we store our media files.
804
     *
805
     * @return FilesystemInterface
806
     */
807
    public function mediaFilesystem(): FilesystemInterface
808
    {
809
        $media_dir  = $this->getPreference('MEDIA_DIRECTORY', 'media/');
810
        $filesystem = app(FilesystemInterface::class);
811
        $adapter    = new ChrootAdapter($filesystem, $media_dir);
812
813
        return new Filesystem($adapter);
814
    }
815
}
816