Completed
Push — master ( 4a8100...5cd281 )
by Greg
07:25
created

Tree::importGedcomFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2021 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees;
21
22
use Closure;
23
use Fisharebest\Flysystem\Adapter\ChrootAdapter;
24
use Fisharebest\Webtrees\Contracts\UserInterface;
25
use Fisharebest\Webtrees\Services\GedcomExportService;
26
use Fisharebest\Webtrees\Services\PendingChangesService;
27
use Fisharebest\Webtrees\Services\TreeService;
28
use Illuminate\Database\Capsule\Manager as DB;
29
use InvalidArgumentException;
30
use League\Flysystem\Filesystem;
31
use League\Flysystem\FilesystemInterface;
32
use Psr\Http\Message\StreamInterface;
33
use stdClass;
34
35
use function app;
36
use function array_key_exists;
37
use function date;
1 ignored issue
show
Bug introduced by
This use statement conflicts with another class in this namespace, Fisharebest\Webtrees\date. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
38
use function str_starts_with;
39
use function strlen;
40
use function strtoupper;
41
use function substr;
42
use function substr_replace;
43
44
/**
45
 * Provide an interface to the wt_gedcom table.
46
 */
47
class Tree
48
{
49
    private const RESN_PRIVACY = [
50
        'none'         => Auth::PRIV_PRIVATE,
51
        'privacy'      => Auth::PRIV_USER,
52
        'confidential' => Auth::PRIV_NONE,
53
        'hidden'       => Auth::PRIV_HIDE,
54
    ];
55
56
    /** @var int The tree's ID number */
57
    private $id;
58
59
    /** @var string The tree's name */
60
    private $name;
61
62
    /** @var string The tree's title */
63
    private $title;
64
65
    /** @var int[] Default access rules for facts in this tree */
66
    private $fact_privacy;
67
68
    /** @var int[] Default access rules for individuals in this tree */
69
    private $individual_privacy;
70
71
    /** @var integer[][] Default access rules for individual facts in this tree */
72
    private $individual_fact_privacy;
73
74
    /** @var string[] Cached copy of the wt_gedcom_setting table. */
75
    private $preferences = [];
76
77
    /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */
78
    private $user_preferences = [];
79
80
    /**
81
     * Create a tree object.
82
     *
83
     * @param int    $id
84
     * @param string $name
85
     * @param string $title
86
     */
87
    public function __construct(int $id, string $name, string $title)
88
    {
89
        $this->id                      = $id;
90
        $this->name                    = $name;
91
        $this->title                   = $title;
92
        $this->fact_privacy            = [];
93
        $this->individual_privacy      = [];
94
        $this->individual_fact_privacy = [];
95
96
        // Load the privacy settings for this tree
97
        $rows = DB::table('default_resn')
98
            ->where('gedcom_id', '=', $this->id)
99
            ->get();
100
101
        foreach ($rows as $row) {
102
            // Convert GEDCOM privacy restriction to a webtrees access level.
103
            $row->resn = self::RESN_PRIVACY[$row->resn];
104
105
            if ($row->xref !== null) {
106
                if ($row->tag_type !== null) {
107
                    $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn;
108
                } else {
109
                    $this->individual_privacy[$row->xref] = $row->resn;
110
                }
111
            } else {
112
                $this->fact_privacy[$row->tag_type] = $row->resn;
113
            }
114
        }
115
    }
116
117
    /**
118
     * A closure which will create a record from a database row.
119
     *
120
     * @return Closure
121
     */
122
    public static function rowMapper(): Closure
123
    {
124
        return static function (stdClass $row): Tree {
125
            return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title);
126
        };
127
    }
128
129
    /**
130
     * Set the tree’s configuration settings.
131
     *
132
     * @param string $setting_name
133
     * @param string $setting_value
134
     *
135
     * @return $this
136
     */
137
    public function setPreference(string $setting_name, string $setting_value): Tree
138
    {
139
        if ($setting_value !== $this->getPreference($setting_name)) {
140
            DB::table('gedcom_setting')->updateOrInsert([
141
                'gedcom_id'    => $this->id,
142
                'setting_name' => $setting_name,
143
            ], [
144
                'setting_value' => $setting_value,
145
            ]);
146
147
            $this->preferences[$setting_name] = $setting_value;
148
149
            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this);
150
        }
151
152
        return $this;
153
    }
154
155
    /**
156
     * Get the tree’s configuration settings.
157
     *
158
     * @param string $setting_name
159
     * @param string $default
160
     *
161
     * @return string
162
     */
163
    public function getPreference(string $setting_name, string $default = ''): string
164
    {
165
        if ($this->preferences === []) {
166
            $this->preferences = DB::table('gedcom_setting')
167
                ->where('gedcom_id', '=', $this->id)
168
                ->pluck('setting_value', 'setting_name')
169
                ->all();
170
        }
171
172
        return $this->preferences[$setting_name] ?? $default;
173
    }
174
175
    /**
176
     * The name of this tree
177
     *
178
     * @return string
179
     */
180
    public function name(): string
181
    {
182
        return $this->name;
183
    }
184
185
    /**
186
     * The title of this tree
187
     *
188
     * @return string
189
     */
190
    public function title(): string
191
    {
192
        return $this->title;
193
    }
194
195
    /**
196
     * The fact-level privacy for this tree.
197
     *
198
     * @return int[]
199
     */
200
    public function getFactPrivacy(): array
201
    {
202
        return $this->fact_privacy;
203
    }
204
205
    /**
206
     * The individual-level privacy for this tree.
207
     *
208
     * @return int[]
209
     */
210
    public function getIndividualPrivacy(): array
211
    {
212
        return $this->individual_privacy;
213
    }
214
215
    /**
216
     * The individual-fact-level privacy for this tree.
217
     *
218
     * @return int[][]
219
     */
220
    public function getIndividualFactPrivacy(): array
221
    {
222
        return $this->individual_fact_privacy;
223
    }
224
225
    /**
226
     * Set the tree’s user-configuration settings.
227
     *
228
     * @param UserInterface $user
229
     * @param string        $setting_name
230
     * @param string        $setting_value
231
     *
232
     * @return $this
233
     */
234
    public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree
235
    {
236
        if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
237
            // Update the database
238
            DB::table('user_gedcom_setting')->updateOrInsert([
239
                'gedcom_id'    => $this->id(),
240
                'user_id'      => $user->id(),
241
                'setting_name' => $setting_name,
242
            ], [
243
                'setting_value' => $setting_value,
244
            ]);
245
246
            // Update the cache
247
            $this->user_preferences[$user->id()][$setting_name] = $setting_value;
248
            // Audit log of changes
249
            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this);
250
        }
251
252
        return $this;
253
    }
254
255
    /**
256
     * Get the tree’s user-configuration settings.
257
     *
258
     * @param UserInterface $user
259
     * @param string        $setting_name
260
     * @param string        $default
261
     *
262
     * @return string
263
     */
264
    public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string
265
    {
266
        // There are lots of settings, and we need to fetch lots of them on every page
267
        // so it is quicker to fetch them all in one go.
268
        if (!array_key_exists($user->id(), $this->user_preferences)) {
269
            $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting')
270
                ->where('user_id', '=', $user->id())
271
                ->where('gedcom_id', '=', $this->id)
272
                ->pluck('setting_value', 'setting_name')
273
                ->all();
274
        }
275
276
        return $this->user_preferences[$user->id()][$setting_name] ?? $default;
277
    }
278
279
    /**
280
     * The ID of this tree
281
     *
282
     * @return int
283
     */
284
    public function id(): int
285
    {
286
        return $this->id;
287
    }
288
289
    /**
290
     * Can a user accept changes for this tree?
291
     *
292
     * @param UserInterface $user
293
     *
294
     * @return bool
295
     */
296
    public function canAcceptChanges(UserInterface $user): bool
297
    {
298
        return Auth::isModerator($this, $user);
299
    }
300
301
    /**
302
     * Are there any pending edits for this tree, than need reviewing by a moderator.
303
     *
304
     * @return bool
305
     */
306
    public function hasPendingEdit(): bool
307
    {
308
        return DB::table('change')
309
            ->where('gedcom_id', '=', $this->id)
310
            ->where('status', '=', 'pending')
311
            ->exists();
312
    }
313
314
    /**
315
     * Delete everything relating to a tree
316
     *
317
     * @return void
318
     *
319
     * @deprecated - since 2.0.12 - will be removed in 2.1.0
320
     */
321
    public function delete(): void
322
    {
323
        $tree_service = new TreeService();
324
325
        $tree_service->delete($this);
326
    }
327
328
    /**
329
     * Delete all the genealogy data from a tree - in preparation for importing
330
     * new data. Optionally retain the media data, for when the user has been
331
     * editing their data offline using an application which deletes (or does not
332
     * support) media data.
333
     *
334
     * @param bool $keep_media
335
     *
336
     * @return void
337
     *
338
     * @deprecated - since 2.0.12 - will be removed in 2.1.0
339
     */
340
    public function deleteGenealogyData(bool $keep_media): void
341
    {
342
        $tree_service = new TreeService();
343
344
        $tree_service->deleteGenealogyData($this, $keep_media);
345
    }
346
347
    /**
348
     * Export the tree to a GEDCOM file
349
     *
350
     * @param resource $stream
351
     *
352
     * @return void
353
     *
354
     * @deprecated since 2.0.5.  Will be removed in 2.1.0
355
     */
356
    public function exportGedcom($stream): void
357
    {
358
        $gedcom_export_service = new GedcomExportService();
359
360
        $gedcom_export_service->export($this, $stream);
361
    }
362
363
    /**
364
     * Import data from a gedcom file into this tree.
365
     *
366
     * @param StreamInterface $stream   The GEDCOM file.
367
     * @param string          $filename The preferred filename, for export/download.
368
     *
369
     * @return void
370
     *
371
     * @deprecated since 2.0.12.  Will be removed in 2.1.0
372
     */
373
    public function importGedcomFile(StreamInterface $stream, string $filename): void
374
    {
375
        $tree_service = new TreeService();
376
377
        $tree_service->importGedcomFile($this, $stream, $filename);
378
    }
379
380
    /**
381
     * Create a new record from GEDCOM data.
382
     *
383
     * @param string $gedcom
384
     *
385
     * @return GedcomRecord|Individual|Family|Location|Note|Source|Repository|Media|Submitter|Submission
386
     * @throws InvalidArgumentException
387
     */
388
    public function createRecord(string $gedcom): GedcomRecord
389
    {
390
        if (!preg_match('/^0 @@ ([_A-Z]+)/', $gedcom, $match)) {
391
            throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@');
392
        }
393
394
        $xref   = Registry::xrefFactory()->make($match[1]);
395
        $gedcom = substr_replace($gedcom, $xref, 3, 0);
396
397
        // Create a change record
398
        $today = strtoupper(date('d M Y'));
399
        $now   = date('H:i:s');
400
        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
401
402
        // Create a pending change
403
        DB::table('change')->insert([
404
            'gedcom_id'  => $this->id,
405
            'xref'       => $xref,
406
            'old_gedcom' => '',
407
            'new_gedcom' => $gedcom,
408
            'user_id'    => Auth::id(),
409
        ]);
410
411
        // Accept this pending change
412
        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
413
            $record = Registry::gedcomRecordFactory()->new($xref, $gedcom, null, $this);
414
415
            app(PendingChangesService::class)->acceptRecord($record);
416
417
            return $record;
418
        }
419
420
        return Registry::gedcomRecordFactory()->new($xref, '', $gedcom, $this);
421
    }
422
423
    /**
424
     * Generate a new XREF, unique across all family trees
425
     *
426
     * @return string
427
     * @deprecated - use the factory directly.
428
     */
429
    public function getNewXref(): string
430
    {
431
        return Registry::xrefFactory()->make(GedcomRecord::RECORD_TYPE);
432
    }
433
434
    /**
435
     * Create a new family from GEDCOM data.
436
     *
437
     * @param string $gedcom
438
     *
439
     * @return Family
440
     * @throws InvalidArgumentException
441
     */
442
    public function createFamily(string $gedcom): GedcomRecord
443
    {
444
        if (!str_starts_with($gedcom, '0 @@ FAM')) {
445
            throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM');
446
        }
447
448
        $xref   = Registry::xrefFactory()->make(Family::RECORD_TYPE);
449
        $gedcom = substr_replace($gedcom, $xref, 3, 0);
450
451
        // Create a change record
452
        $today = strtoupper(date('d M Y'));
453
        $now   = date('H:i:s');
454
        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
455
456
        // Create a pending change
457
        DB::table('change')->insert([
458
            'gedcom_id'  => $this->id,
459
            'xref'       => $xref,
460
            'old_gedcom' => '',
461
            'new_gedcom' => $gedcom,
462
            'user_id'    => Auth::id(),
463
        ]);
464
465
        // Accept this pending change
466
        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
467
            $record = Registry::familyFactory()->new($xref, $gedcom, null, $this);
468
469
            app(PendingChangesService::class)->acceptRecord($record);
470
471
            return $record;
472
        }
473
474
        return Registry::familyFactory()->new($xref, '', $gedcom, $this);
475
    }
476
477
    /**
478
     * Create a new individual from GEDCOM data.
479
     *
480
     * @param string $gedcom
481
     *
482
     * @return Individual
483
     * @throws InvalidArgumentException
484
     */
485
    public function createIndividual(string $gedcom): GedcomRecord
486
    {
487
        if (!str_starts_with($gedcom, '0 @@ INDI')) {
488
            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI');
489
        }
490
491
        $xref   = Registry::xrefFactory()->make(Individual::RECORD_TYPE);
492
        $gedcom = substr_replace($gedcom, $xref, 3, 0);
493
494
        // Create a change record
495
        $today = strtoupper(date('d M Y'));
496
        $now   = date('H:i:s');
497
        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
498
499
        // Create a pending change
500
        DB::table('change')->insert([
501
            'gedcom_id'  => $this->id,
502
            'xref'       => $xref,
503
            'old_gedcom' => '',
504
            'new_gedcom' => $gedcom,
505
            'user_id'    => Auth::id(),
506
        ]);
507
508
        // Accept this pending change
509
        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
510
            $record = Registry::individualFactory()->new($xref, $gedcom, null, $this);
511
512
            app(PendingChangesService::class)->acceptRecord($record);
513
514
            return $record;
515
        }
516
517
        return Registry::individualFactory()->new($xref, '', $gedcom, $this);
518
    }
519
520
    /**
521
     * Create a new media object from GEDCOM data.
522
     *
523
     * @param string $gedcom
524
     *
525
     * @return Media
526
     * @throws InvalidArgumentException
527
     */
528
    public function createMediaObject(string $gedcom): Media
529
    {
530
        if (!str_starts_with($gedcom, '0 @@ OBJE')) {
531
            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE');
532
        }
533
534
        $xref   = Registry::xrefFactory()->make(Media::RECORD_TYPE);
535
        $gedcom = substr_replace($gedcom, $xref, 3, 0);
536
537
        // Create a change record
538
        $today = strtoupper(date('d M Y'));
539
        $now   = date('H:i:s');
540
        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
541
542
        // Create a pending change
543
        DB::table('change')->insert([
544
            'gedcom_id'  => $this->id,
545
            'xref'       => $xref,
546
            'old_gedcom' => '',
547
            'new_gedcom' => $gedcom,
548
            'user_id'    => Auth::id(),
549
        ]);
550
551
        // Accept this pending change
552
        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
553
            $record = Registry::mediaFactory()->new($xref, $gedcom, null, $this);
554
555
            app(PendingChangesService::class)->acceptRecord($record);
556
557
            return $record;
558
        }
559
560
        return Registry::mediaFactory()->new($xref, '', $gedcom, $this);
561
    }
562
563
    /**
564
     * What is the most significant individual in this tree.
565
     *
566
     * @param UserInterface $user
567
     * @param string        $xref
568
     *
569
     * @return Individual
570
     */
571
    public function significantIndividual(UserInterface $user, $xref = ''): Individual
572
    {
573
        if ($xref === '') {
574
            $individual = null;
575
        } else {
576
            $individual = Registry::individualFactory()->make($xref, $this);
577
578
            if ($individual === null) {
579
                $family = Registry::familyFactory()->make($xref, $this);
580
581
                if ($family instanceof Family) {
582
                    $individual = $family->spouses()->first() ?? $family->children()->first();
583
                }
584
            }
585
        }
586
587
        if ($individual === null && $this->getUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF) !== '') {
588
            $individual = Registry::individualFactory()->make($this->getUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF), $this);
589
        }
590
591
        if ($individual === null && $this->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF) !== '') {
592
            $individual = Registry::individualFactory()->make($this->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF), $this);
593
        }
594
595
        if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') {
596
            $individual = Registry::individualFactory()->make($this->getPreference('PEDIGREE_ROOT_ID'), $this);
597
        }
598
        if ($individual === null) {
599
            $xref = (string) DB::table('individuals')
600
                ->where('i_file', '=', $this->id())
601
                ->min('i_id');
602
603
            $individual = Registry::individualFactory()->make($xref, $this);
604
        }
605
        if ($individual === null) {
606
            // always return a record
607
            $individual = Registry::individualFactory()->new('I', '0 @I@ INDI', null, $this);
608
        }
609
610
        return $individual;
611
    }
612
613
    /**
614
     * Where do we store our media files.
615
     *
616
     * @param FilesystemInterface $data_filesystem
617
     *
618
     * @return FilesystemInterface
619
     */
620
    public function mediaFilesystem(FilesystemInterface $data_filesystem): FilesystemInterface
621
    {
622
        $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/');
623
        $adapter   = new ChrootAdapter($data_filesystem, $media_dir);
624
625
        return new Filesystem($adapter);
626
    }
627
}
628