xref: /webtrees/app/Tree.php (revision f7cf8a155e2743f3d124eef3d30a558ab062fa4b)
1a25f0a04SGreg Roach<?php
23976b470SGreg Roach
3a25f0a04SGreg Roach/**
4a25f0a04SGreg Roach * webtrees: online genealogy
51fe542e9SGreg Roach * Copyright (C) 2021 webtrees development team
6a25f0a04SGreg Roach * This program is free software: you can redistribute it and/or modify
7a25f0a04SGreg Roach * it under the terms of the GNU General Public License as published by
8a25f0a04SGreg Roach * the Free Software Foundation, either version 3 of the License, or
9a25f0a04SGreg Roach * (at your option) any later version.
10a25f0a04SGreg Roach * This program is distributed in the hope that it will be useful,
11a25f0a04SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12a25f0a04SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13a25f0a04SGreg Roach * GNU General Public License for more details.
14a25f0a04SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16a25f0a04SGreg Roach */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
2076692c8bSGreg Roachnamespace Fisharebest\Webtrees;
21a25f0a04SGreg Roach
225afbc57aSGreg Roachuse Closure;
23456d0d35SGreg Roachuse Fisharebest\Flysystem\Adapter\ChrootAdapter;
24e5a6b4d4SGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface;
2569c05a6eSGreg Roachuse Fisharebest\Webtrees\Services\GedcomExportService;
2622e73debSGreg Roachuse Fisharebest\Webtrees\Services\PendingChangesService;
275cd281f4SGreg Roachuse Fisharebest\Webtrees\Services\TreeService;
2801461f86SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
29afb591d7SGreg Roachuse InvalidArgumentException;
301df7ae39SGreg Roachuse League\Flysystem\Filesystem;
31*f7cf8a15SGreg Roachuse League\Flysystem\FilesystemOperator;
326ccdf4f0SGreg Roachuse Psr\Http\Message\StreamInterface;
338b67c11aSGreg Roachuse stdClass;
34a25f0a04SGreg Roach
351e653452SGreg Roachuse function app;
36dec352c1SGreg Roachuse function array_key_exists;
3753432476SGreg Roachuse function date;
38dec352c1SGreg Roachuse function str_starts_with;
39dec352c1SGreg Roachuse function strlen;
4053432476SGreg Roachuse function strtoupper;
41dec352c1SGreg Roachuse function substr;
42dec352c1SGreg Roachuse function substr_replace;
431e653452SGreg Roach
44a25f0a04SGreg Roach/**
4576692c8bSGreg Roach * Provide an interface to the wt_gedcom table.
46a25f0a04SGreg Roach */
47c1010edaSGreg Roachclass Tree
48c1010edaSGreg Roach{
49061b43d7SGreg Roach    private const RESN_PRIVACY = [
50061b43d7SGreg Roach        'none'         => Auth::PRIV_PRIVATE,
51061b43d7SGreg Roach        'privacy'      => Auth::PRIV_USER,
52061b43d7SGreg Roach        'confidential' => Auth::PRIV_NONE,
53061b43d7SGreg Roach        'hidden'       => Auth::PRIV_HIDE,
54061b43d7SGreg Roach    ];
553df1e584SGreg Roach
566ccdf4f0SGreg Roach    /** @var int The tree's ID number */
576ccdf4f0SGreg Roach    private $id;
583df1e584SGreg Roach
596ccdf4f0SGreg Roach    /** @var string The tree's name */
606ccdf4f0SGreg Roach    private $name;
613df1e584SGreg Roach
626ccdf4f0SGreg Roach    /** @var string The tree's title */
636ccdf4f0SGreg Roach    private $title;
643df1e584SGreg Roach
656ccdf4f0SGreg Roach    /** @var int[] Default access rules for facts in this tree */
666ccdf4f0SGreg Roach    private $fact_privacy;
673df1e584SGreg Roach
686ccdf4f0SGreg Roach    /** @var int[] Default access rules for individuals in this tree */
696ccdf4f0SGreg Roach    private $individual_privacy;
703df1e584SGreg Roach
716ccdf4f0SGreg Roach    /** @var integer[][] Default access rules for individual facts in this tree */
726ccdf4f0SGreg Roach    private $individual_fact_privacy;
733df1e584SGreg Roach
746ccdf4f0SGreg Roach    /** @var string[] Cached copy of the wt_gedcom_setting table. */
756ccdf4f0SGreg Roach    private $preferences = [];
763df1e584SGreg Roach
776ccdf4f0SGreg Roach    /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */
786ccdf4f0SGreg Roach    private $user_preferences = [];
79061b43d7SGreg Roach
80a25f0a04SGreg Roach    /**
813df1e584SGreg Roach     * Create a tree object.
82a25f0a04SGreg Roach     *
8372cf66d4SGreg Roach     * @param int    $id
84aa6f03bbSGreg Roach     * @param string $name
85cc13d6d8SGreg Roach     * @param string $title
86a25f0a04SGreg Roach     */
875afbc57aSGreg Roach    public function __construct(int $id, string $name, string $title)
88c1010edaSGreg Roach    {
8972cf66d4SGreg Roach        $this->id                      = $id;
90aa6f03bbSGreg Roach        $this->name                    = $name;
91cc13d6d8SGreg Roach        $this->title                   = $title;
9213abd6f3SGreg Roach        $this->fact_privacy            = [];
9313abd6f3SGreg Roach        $this->individual_privacy      = [];
9413abd6f3SGreg Roach        $this->individual_fact_privacy = [];
95518bbdc1SGreg Roach
96518bbdc1SGreg Roach        // Load the privacy settings for this tree
97061b43d7SGreg Roach        $rows = DB::table('default_resn')
98061b43d7SGreg Roach            ->where('gedcom_id', '=', $this->id)
99061b43d7SGreg Roach            ->get();
100518bbdc1SGreg Roach
101518bbdc1SGreg Roach        foreach ($rows as $row) {
102061b43d7SGreg Roach            // Convert GEDCOM privacy restriction to a webtrees access level.
103061b43d7SGreg Roach            $row->resn = self::RESN_PRIVACY[$row->resn];
104061b43d7SGreg Roach
105518bbdc1SGreg Roach            if ($row->xref !== null) {
106518bbdc1SGreg Roach                if ($row->tag_type !== null) {
107b262b3d3SGreg Roach                    $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn;
108518bbdc1SGreg Roach                } else {
109b262b3d3SGreg Roach                    $this->individual_privacy[$row->xref] = $row->resn;
110518bbdc1SGreg Roach                }
111518bbdc1SGreg Roach            } else {
112b262b3d3SGreg Roach                $this->fact_privacy[$row->tag_type] = $row->resn;
113518bbdc1SGreg Roach            }
114518bbdc1SGreg Roach        }
115a25f0a04SGreg Roach    }
116a25f0a04SGreg Roach
117a25f0a04SGreg Roach    /**
1185afbc57aSGreg Roach     * A closure which will create a record from a database row.
1195afbc57aSGreg Roach     *
1205afbc57aSGreg Roach     * @return Closure
1215afbc57aSGreg Roach     */
1225afbc57aSGreg Roach    public static function rowMapper(): Closure
1235afbc57aSGreg Roach    {
1245afbc57aSGreg Roach        return static function (stdClass $row): Tree {
1255afbc57aSGreg Roach            return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title);
1265afbc57aSGreg Roach        };
1275afbc57aSGreg Roach    }
1285afbc57aSGreg Roach
1295afbc57aSGreg Roach    /**
1306ccdf4f0SGreg Roach     * Set the tree’s configuration settings.
1316ccdf4f0SGreg Roach     *
1326ccdf4f0SGreg Roach     * @param string $setting_name
1336ccdf4f0SGreg Roach     * @param string $setting_value
1346ccdf4f0SGreg Roach     *
1356ccdf4f0SGreg Roach     * @return $this
1366ccdf4f0SGreg Roach     */
1376ccdf4f0SGreg Roach    public function setPreference(string $setting_name, string $setting_value): Tree
1386ccdf4f0SGreg Roach    {
1396ccdf4f0SGreg Roach        if ($setting_value !== $this->getPreference($setting_name)) {
1406ccdf4f0SGreg Roach            DB::table('gedcom_setting')->updateOrInsert([
1416ccdf4f0SGreg Roach                'gedcom_id'    => $this->id,
1426ccdf4f0SGreg Roach                'setting_name' => $setting_name,
1436ccdf4f0SGreg Roach            ], [
1446ccdf4f0SGreg Roach                'setting_value' => $setting_value,
1456ccdf4f0SGreg Roach            ]);
1466ccdf4f0SGreg Roach
1476ccdf4f0SGreg Roach            $this->preferences[$setting_name] = $setting_value;
1486ccdf4f0SGreg Roach
1496ccdf4f0SGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this);
1506ccdf4f0SGreg Roach        }
1516ccdf4f0SGreg Roach
1526ccdf4f0SGreg Roach        return $this;
1536ccdf4f0SGreg Roach    }
1546ccdf4f0SGreg Roach
1556ccdf4f0SGreg Roach    /**
1566ccdf4f0SGreg Roach     * Get the tree’s configuration settings.
1576ccdf4f0SGreg Roach     *
1586ccdf4f0SGreg Roach     * @param string $setting_name
1596ccdf4f0SGreg Roach     * @param string $default
1606ccdf4f0SGreg Roach     *
1616ccdf4f0SGreg Roach     * @return string
1626ccdf4f0SGreg Roach     */
1636ccdf4f0SGreg Roach    public function getPreference(string $setting_name, string $default = ''): string
1646ccdf4f0SGreg Roach    {
16554c1ab5eSGreg Roach        if ($this->preferences === []) {
1666ccdf4f0SGreg Roach            $this->preferences = DB::table('gedcom_setting')
1676ccdf4f0SGreg Roach                ->where('gedcom_id', '=', $this->id)
1686ccdf4f0SGreg Roach                ->pluck('setting_value', 'setting_name')
1696ccdf4f0SGreg Roach                ->all();
1706ccdf4f0SGreg Roach        }
1716ccdf4f0SGreg Roach
1726ccdf4f0SGreg Roach        return $this->preferences[$setting_name] ?? $default;
1736ccdf4f0SGreg Roach    }
1746ccdf4f0SGreg Roach
1756ccdf4f0SGreg Roach    /**
1766ccdf4f0SGreg Roach     * The name of this tree
1776ccdf4f0SGreg Roach     *
1786ccdf4f0SGreg Roach     * @return string
1796ccdf4f0SGreg Roach     */
1806ccdf4f0SGreg Roach    public function name(): string
1816ccdf4f0SGreg Roach    {
1826ccdf4f0SGreg Roach        return $this->name;
1836ccdf4f0SGreg Roach    }
1846ccdf4f0SGreg Roach
1856ccdf4f0SGreg Roach    /**
1866ccdf4f0SGreg Roach     * The title of this tree
1876ccdf4f0SGreg Roach     *
1886ccdf4f0SGreg Roach     * @return string
1896ccdf4f0SGreg Roach     */
1906ccdf4f0SGreg Roach    public function title(): string
1916ccdf4f0SGreg Roach    {
1926ccdf4f0SGreg Roach        return $this->title;
1936ccdf4f0SGreg Roach    }
1946ccdf4f0SGreg Roach
1956ccdf4f0SGreg Roach    /**
1966ccdf4f0SGreg Roach     * The fact-level privacy for this tree.
1976ccdf4f0SGreg Roach     *
1986ccdf4f0SGreg Roach     * @return int[]
1996ccdf4f0SGreg Roach     */
2006ccdf4f0SGreg Roach    public function getFactPrivacy(): array
2016ccdf4f0SGreg Roach    {
2026ccdf4f0SGreg Roach        return $this->fact_privacy;
2036ccdf4f0SGreg Roach    }
2046ccdf4f0SGreg Roach
2056ccdf4f0SGreg Roach    /**
2066ccdf4f0SGreg Roach     * The individual-level privacy for this tree.
2076ccdf4f0SGreg Roach     *
2086ccdf4f0SGreg Roach     * @return int[]
2096ccdf4f0SGreg Roach     */
2106ccdf4f0SGreg Roach    public function getIndividualPrivacy(): array
2116ccdf4f0SGreg Roach    {
2126ccdf4f0SGreg Roach        return $this->individual_privacy;
2136ccdf4f0SGreg Roach    }
2146ccdf4f0SGreg Roach
2156ccdf4f0SGreg Roach    /**
2166ccdf4f0SGreg Roach     * The individual-fact-level privacy for this tree.
2176ccdf4f0SGreg Roach     *
2186ccdf4f0SGreg Roach     * @return int[][]
2196ccdf4f0SGreg Roach     */
2206ccdf4f0SGreg Roach    public function getIndividualFactPrivacy(): array
2216ccdf4f0SGreg Roach    {
2226ccdf4f0SGreg Roach        return $this->individual_fact_privacy;
2236ccdf4f0SGreg Roach    }
2246ccdf4f0SGreg Roach
2256ccdf4f0SGreg Roach    /**
2266ccdf4f0SGreg Roach     * Set the tree’s user-configuration settings.
2276ccdf4f0SGreg Roach     *
2286ccdf4f0SGreg Roach     * @param UserInterface $user
2296ccdf4f0SGreg Roach     * @param string        $setting_name
2306ccdf4f0SGreg Roach     * @param string        $setting_value
2316ccdf4f0SGreg Roach     *
2326ccdf4f0SGreg Roach     * @return $this
2336ccdf4f0SGreg Roach     */
2346ccdf4f0SGreg Roach    public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree
2356ccdf4f0SGreg Roach    {
2366ccdf4f0SGreg Roach        if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
2376ccdf4f0SGreg Roach            // Update the database
2386ccdf4f0SGreg Roach            DB::table('user_gedcom_setting')->updateOrInsert([
2396ccdf4f0SGreg Roach                'gedcom_id'    => $this->id(),
2406ccdf4f0SGreg Roach                'user_id'      => $user->id(),
2416ccdf4f0SGreg Roach                'setting_name' => $setting_name,
2426ccdf4f0SGreg Roach            ], [
2436ccdf4f0SGreg Roach                'setting_value' => $setting_value,
2446ccdf4f0SGreg Roach            ]);
2456ccdf4f0SGreg Roach
2466ccdf4f0SGreg Roach            // Update the cache
2476ccdf4f0SGreg Roach            $this->user_preferences[$user->id()][$setting_name] = $setting_value;
2486ccdf4f0SGreg Roach            // Audit log of changes
2496ccdf4f0SGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this);
2506ccdf4f0SGreg Roach        }
2516ccdf4f0SGreg Roach
2526ccdf4f0SGreg Roach        return $this;
2536ccdf4f0SGreg Roach    }
2546ccdf4f0SGreg Roach
2556ccdf4f0SGreg Roach    /**
2566ccdf4f0SGreg Roach     * Get the tree’s user-configuration settings.
2576ccdf4f0SGreg Roach     *
2586ccdf4f0SGreg Roach     * @param UserInterface $user
2596ccdf4f0SGreg Roach     * @param string        $setting_name
2606ccdf4f0SGreg Roach     * @param string        $default
2616ccdf4f0SGreg Roach     *
2626ccdf4f0SGreg Roach     * @return string
2636ccdf4f0SGreg Roach     */
2646ccdf4f0SGreg Roach    public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string
2656ccdf4f0SGreg Roach    {
2666ccdf4f0SGreg Roach        // There are lots of settings, and we need to fetch lots of them on every page
2676ccdf4f0SGreg Roach        // so it is quicker to fetch them all in one go.
2686ccdf4f0SGreg Roach        if (!array_key_exists($user->id(), $this->user_preferences)) {
2696ccdf4f0SGreg Roach            $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting')
2706ccdf4f0SGreg Roach                ->where('user_id', '=', $user->id())
2716ccdf4f0SGreg Roach                ->where('gedcom_id', '=', $this->id)
2726ccdf4f0SGreg Roach                ->pluck('setting_value', 'setting_name')
2736ccdf4f0SGreg Roach                ->all();
2746ccdf4f0SGreg Roach        }
2756ccdf4f0SGreg Roach
2766ccdf4f0SGreg Roach        return $this->user_preferences[$user->id()][$setting_name] ?? $default;
2776ccdf4f0SGreg Roach    }
2786ccdf4f0SGreg Roach
2796ccdf4f0SGreg Roach    /**
2806ccdf4f0SGreg Roach     * The ID of this tree
2816ccdf4f0SGreg Roach     *
2826ccdf4f0SGreg Roach     * @return int
2836ccdf4f0SGreg Roach     */
2846ccdf4f0SGreg Roach    public function id(): int
2856ccdf4f0SGreg Roach    {
2866ccdf4f0SGreg Roach        return $this->id;
2876ccdf4f0SGreg Roach    }
2886ccdf4f0SGreg Roach
2896ccdf4f0SGreg Roach    /**
2906ccdf4f0SGreg Roach     * Can a user accept changes for this tree?
2916ccdf4f0SGreg Roach     *
2926ccdf4f0SGreg Roach     * @param UserInterface $user
2936ccdf4f0SGreg Roach     *
2946ccdf4f0SGreg Roach     * @return bool
2956ccdf4f0SGreg Roach     */
2966ccdf4f0SGreg Roach    public function canAcceptChanges(UserInterface $user): bool
2976ccdf4f0SGreg Roach    {
2986ccdf4f0SGreg Roach        return Auth::isModerator($this, $user);
2996ccdf4f0SGreg Roach    }
3006ccdf4f0SGreg Roach
3016ccdf4f0SGreg Roach    /**
302b78374c5SGreg Roach     * Are there any pending edits for this tree, than need reviewing by a moderator.
303b78374c5SGreg Roach     *
304b78374c5SGreg Roach     * @return bool
305b78374c5SGreg Roach     */
306771ae10aSGreg Roach    public function hasPendingEdit(): bool
307c1010edaSGreg Roach    {
30815a3f100SGreg Roach        return DB::table('change')
30915a3f100SGreg Roach            ->where('gedcom_id', '=', $this->id)
31015a3f100SGreg Roach            ->where('status', '=', 'pending')
31115a3f100SGreg Roach            ->exists();
312b78374c5SGreg Roach    }
313b78374c5SGreg Roach
314b78374c5SGreg Roach    /**
3156ccdf4f0SGreg Roach     * Delete everything relating to a tree
3166ccdf4f0SGreg Roach     *
3176ccdf4f0SGreg Roach     * @return void
3185cd281f4SGreg Roach     *
3195cd281f4SGreg Roach     * @deprecated - since 2.0.12 - will be removed in 2.1.0
3206ccdf4f0SGreg Roach     */
3216ccdf4f0SGreg Roach    public function delete(): void
3226ccdf4f0SGreg Roach    {
3235cd281f4SGreg Roach        $tree_service = new TreeService();
3246ccdf4f0SGreg Roach
3255cd281f4SGreg Roach        $tree_service->delete($this);
3266ccdf4f0SGreg Roach    }
3276ccdf4f0SGreg Roach
3286ccdf4f0SGreg Roach    /**
329a25f0a04SGreg Roach     * Delete all the genealogy data from a tree - in preparation for importing
330a25f0a04SGreg Roach     * new data. Optionally retain the media data, for when the user has been
331a25f0a04SGreg Roach     * editing their data offline using an application which deletes (or does not
332a25f0a04SGreg Roach     * support) media data.
333a25f0a04SGreg Roach     *
334a25f0a04SGreg Roach     * @param bool $keep_media
335b7e60af1SGreg Roach     *
336b7e60af1SGreg Roach     * @return void
3375cd281f4SGreg Roach     *
3385cd281f4SGreg Roach     * @deprecated - since 2.0.12 - will be removed in 2.1.0
339a25f0a04SGreg Roach     */
340e364afe4SGreg Roach    public function deleteGenealogyData(bool $keep_media): void
341c1010edaSGreg Roach    {
3425cd281f4SGreg Roach        $tree_service = new TreeService();
343a25f0a04SGreg Roach
3445cd281f4SGreg Roach        $tree_service->deleteGenealogyData($this, $keep_media);
345a25f0a04SGreg Roach    }
346a25f0a04SGreg Roach
347a25f0a04SGreg Roach    /**
348a25f0a04SGreg Roach     * Export the tree to a GEDCOM file
349a25f0a04SGreg Roach     *
3505792757eSGreg Roach     * @param resource $stream
351b7e60af1SGreg Roach     *
352b7e60af1SGreg Roach     * @return void
35369c05a6eSGreg Roach     *
35469c05a6eSGreg Roach     * @deprecated since 2.0.5.  Will be removed in 2.1.0
355a25f0a04SGreg Roach     */
356425af8b9SGreg Roach    public function exportGedcom($stream): void
357c1010edaSGreg Roach    {
35869c05a6eSGreg Roach        $gedcom_export_service = new GedcomExportService();
35994026f20SGreg Roach
36069c05a6eSGreg Roach        $gedcom_export_service->export($this, $stream);
361a25f0a04SGreg Roach    }
362a25f0a04SGreg Roach
363a25f0a04SGreg Roach    /**
364a25f0a04SGreg Roach     * Import data from a gedcom file into this tree.
365a25f0a04SGreg Roach     *
3666ccdf4f0SGreg Roach     * @param StreamInterface $stream   The GEDCOM file.
367a25f0a04SGreg Roach     * @param string          $filename The preferred filename, for export/download.
368a25f0a04SGreg Roach     *
369b7e60af1SGreg Roach     * @return void
3705cd281f4SGreg Roach     *
3715cd281f4SGreg Roach     * @deprecated since 2.0.12.  Will be removed in 2.1.0
372a25f0a04SGreg Roach     */
3736ccdf4f0SGreg Roach    public function importGedcomFile(StreamInterface $stream, string $filename): void
374c1010edaSGreg Roach    {
3755cd281f4SGreg Roach        $tree_service = new TreeService();
376a25f0a04SGreg Roach
3775cd281f4SGreg Roach        $tree_service->importGedcomFile($this, $stream, $filename);
3786ccdf4f0SGreg Roach    }
3796ccdf4f0SGreg Roach
3806ccdf4f0SGreg Roach    /**
3816ccdf4f0SGreg Roach     * Create a new record from GEDCOM data.
3826ccdf4f0SGreg Roach     *
3836ccdf4f0SGreg Roach     * @param string $gedcom
3846ccdf4f0SGreg Roach     *
3850d15532eSGreg Roach     * @return GedcomRecord|Individual|Family|Location|Note|Source|Repository|Media|Submitter|Submission
3866ccdf4f0SGreg Roach     * @throws InvalidArgumentException
3876ccdf4f0SGreg Roach     */
3886ccdf4f0SGreg Roach    public function createRecord(string $gedcom): GedcomRecord
3896ccdf4f0SGreg Roach    {
390b4a2f885SGreg Roach        if (!preg_match('/^0 @@ ([_A-Z]+)/', $gedcom, $match)) {
3916ccdf4f0SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@');
3926ccdf4f0SGreg Roach        }
3936ccdf4f0SGreg Roach
3946b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make($match[1]);
395dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
3966ccdf4f0SGreg Roach
3976ccdf4f0SGreg Roach        // Create a change record
39853432476SGreg Roach        $today = strtoupper(date('d M Y'));
39953432476SGreg Roach        $now   = date('H:i:s');
40053432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
4016ccdf4f0SGreg Roach
4026ccdf4f0SGreg Roach        // Create a pending change
4036ccdf4f0SGreg Roach        DB::table('change')->insert([
4046ccdf4f0SGreg Roach            'gedcom_id'  => $this->id,
4056ccdf4f0SGreg Roach            'xref'       => $xref,
4066ccdf4f0SGreg Roach            'old_gedcom' => '',
4076ccdf4f0SGreg Roach            'new_gedcom' => $gedcom,
4086ccdf4f0SGreg Roach            'user_id'    => Auth::id(),
4096ccdf4f0SGreg Roach        ]);
4106ccdf4f0SGreg Roach
4116ccdf4f0SGreg Roach        // Accept this pending change
4121fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
4136b9cb339SGreg Roach            $record = Registry::gedcomRecordFactory()->new($xref, $gedcom, null, $this);
4146ccdf4f0SGreg Roach
41522e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
41622e73debSGreg Roach
41722e73debSGreg Roach            return $record;
4186ccdf4f0SGreg Roach        }
4196ccdf4f0SGreg Roach
4206b9cb339SGreg Roach        return Registry::gedcomRecordFactory()->new($xref, '', $gedcom, $this);
421a25f0a04SGreg Roach    }
422304f20d5SGreg Roach
423304f20d5SGreg Roach    /**
424b90d8accSGreg Roach     * Generate a new XREF, unique across all family trees
425b90d8accSGreg Roach     *
426b90d8accSGreg Roach     * @return string
427b4a2f885SGreg Roach     * @deprecated - use the factory directly.
428b90d8accSGreg Roach     */
429771ae10aSGreg Roach    public function getNewXref(): string
430c1010edaSGreg Roach    {
4316b9cb339SGreg Roach        return Registry::xrefFactory()->make(GedcomRecord::RECORD_TYPE);
432b90d8accSGreg Roach    }
433b90d8accSGreg Roach
434b90d8accSGreg Roach    /**
435afb591d7SGreg Roach     * Create a new family from GEDCOM data.
436afb591d7SGreg Roach     *
437afb591d7SGreg Roach     * @param string $gedcom
438afb591d7SGreg Roach     *
439afb591d7SGreg Roach     * @return Family
440afb591d7SGreg Roach     * @throws InvalidArgumentException
441afb591d7SGreg Roach     */
442afb591d7SGreg Roach    public function createFamily(string $gedcom): GedcomRecord
443afb591d7SGreg Roach    {
444dec352c1SGreg Roach        if (!str_starts_with($gedcom, '0 @@ FAM')) {
445afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM');
446afb591d7SGreg Roach        }
447afb591d7SGreg Roach
4486b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make(Family::RECORD_TYPE);
449dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
450afb591d7SGreg Roach
451afb591d7SGreg Roach        // Create a change record
45253432476SGreg Roach        $today = strtoupper(date('d M Y'));
45353432476SGreg Roach        $now   = date('H:i:s');
45453432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
455afb591d7SGreg Roach
456afb591d7SGreg Roach        // Create a pending change
457963fbaeeSGreg Roach        DB::table('change')->insert([
458963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
459963fbaeeSGreg Roach            'xref'       => $xref,
460963fbaeeSGreg Roach            'old_gedcom' => '',
461963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
462963fbaeeSGreg Roach            'user_id'    => Auth::id(),
463afb591d7SGreg Roach        ]);
464304f20d5SGreg Roach
465304f20d5SGreg Roach        // Accept this pending change
4661fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
4676b9cb339SGreg Roach            $record = Registry::familyFactory()->new($xref, $gedcom, null, $this);
468afb591d7SGreg Roach
46922e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
47022e73debSGreg Roach
47122e73debSGreg Roach            return $record;
472304f20d5SGreg Roach        }
473afb591d7SGreg Roach
4746b9cb339SGreg Roach        return Registry::familyFactory()->new($xref, '', $gedcom, $this);
475afb591d7SGreg Roach    }
476afb591d7SGreg Roach
477afb591d7SGreg Roach    /**
478afb591d7SGreg Roach     * Create a new individual from GEDCOM data.
479afb591d7SGreg Roach     *
480afb591d7SGreg Roach     * @param string $gedcom
481afb591d7SGreg Roach     *
482afb591d7SGreg Roach     * @return Individual
483afb591d7SGreg Roach     * @throws InvalidArgumentException
484afb591d7SGreg Roach     */
485afb591d7SGreg Roach    public function createIndividual(string $gedcom): GedcomRecord
486afb591d7SGreg Roach    {
487dec352c1SGreg Roach        if (!str_starts_with($gedcom, '0 @@ INDI')) {
488afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI');
489afb591d7SGreg Roach        }
490afb591d7SGreg Roach
4916b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make(Individual::RECORD_TYPE);
492dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
493afb591d7SGreg Roach
494afb591d7SGreg Roach        // Create a change record
49553432476SGreg Roach        $today = strtoupper(date('d M Y'));
49653432476SGreg Roach        $now   = date('H:i:s');
49753432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
498afb591d7SGreg Roach
499afb591d7SGreg Roach        // Create a pending change
500963fbaeeSGreg Roach        DB::table('change')->insert([
501963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
502963fbaeeSGreg Roach            'xref'       => $xref,
503963fbaeeSGreg Roach            'old_gedcom' => '',
504963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
505963fbaeeSGreg Roach            'user_id'    => Auth::id(),
506afb591d7SGreg Roach        ]);
507afb591d7SGreg Roach
508afb591d7SGreg Roach        // Accept this pending change
5091fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
5106b9cb339SGreg Roach            $record = Registry::individualFactory()->new($xref, $gedcom, null, $this);
511afb591d7SGreg Roach
51222e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
51322e73debSGreg Roach
51422e73debSGreg Roach            return $record;
515afb591d7SGreg Roach        }
516afb591d7SGreg Roach
5176b9cb339SGreg Roach        return Registry::individualFactory()->new($xref, '', $gedcom, $this);
518304f20d5SGreg Roach    }
5198586983fSGreg Roach
5208586983fSGreg Roach    /**
52120b58d20SGreg Roach     * Create a new media object from GEDCOM data.
52220b58d20SGreg Roach     *
52320b58d20SGreg Roach     * @param string $gedcom
52420b58d20SGreg Roach     *
52520b58d20SGreg Roach     * @return Media
52620b58d20SGreg Roach     * @throws InvalidArgumentException
52720b58d20SGreg Roach     */
52820b58d20SGreg Roach    public function createMediaObject(string $gedcom): Media
52920b58d20SGreg Roach    {
530dec352c1SGreg Roach        if (!str_starts_with($gedcom, '0 @@ OBJE')) {
53120b58d20SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE');
53220b58d20SGreg Roach        }
53320b58d20SGreg Roach
5346b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make(Media::RECORD_TYPE);
535dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
53620b58d20SGreg Roach
53720b58d20SGreg Roach        // Create a change record
53853432476SGreg Roach        $today = strtoupper(date('d M Y'));
53953432476SGreg Roach        $now   = date('H:i:s');
54053432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
54120b58d20SGreg Roach
54220b58d20SGreg Roach        // Create a pending change
543963fbaeeSGreg Roach        DB::table('change')->insert([
544963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
545963fbaeeSGreg Roach            'xref'       => $xref,
546963fbaeeSGreg Roach            'old_gedcom' => '',
547963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
548963fbaeeSGreg Roach            'user_id'    => Auth::id(),
54920b58d20SGreg Roach        ]);
55020b58d20SGreg Roach
55120b58d20SGreg Roach        // Accept this pending change
5521fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
5536b9cb339SGreg Roach            $record = Registry::mediaFactory()->new($xref, $gedcom, null, $this);
55420b58d20SGreg Roach
55522e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
55622e73debSGreg Roach
55722e73debSGreg Roach            return $record;
55820b58d20SGreg Roach        }
55920b58d20SGreg Roach
5606b9cb339SGreg Roach        return Registry::mediaFactory()->new($xref, '', $gedcom, $this);
56120b58d20SGreg Roach    }
56220b58d20SGreg Roach
56320b58d20SGreg Roach    /**
5648586983fSGreg Roach     * What is the most significant individual in this tree.
5658586983fSGreg Roach     *
566e5a6b4d4SGreg Roach     * @param UserInterface $user
5673370567dSGreg Roach     * @param string        $xref
5688586983fSGreg Roach     *
5698586983fSGreg Roach     * @return Individual
5708586983fSGreg Roach     */
5713370567dSGreg Roach    public function significantIndividual(UserInterface $user, $xref = ''): Individual
572c1010edaSGreg Roach    {
5733370567dSGreg Roach        if ($xref === '') {
5748f9b0fb2SGreg Roach            $individual = null;
5753370567dSGreg Roach        } else {
5766b9cb339SGreg Roach            $individual = Registry::individualFactory()->make($xref, $this);
5773370567dSGreg Roach
5783370567dSGreg Roach            if ($individual === null) {
5796b9cb339SGreg Roach                $family = Registry::familyFactory()->make($xref, $this);
5803370567dSGreg Roach
5813370567dSGreg Roach                if ($family instanceof Family) {
5823370567dSGreg Roach                    $individual = $family->spouses()->first() ?? $family->children()->first();
5833370567dSGreg Roach                }
5843370567dSGreg Roach            }
5853370567dSGreg Roach        }
5868586983fSGreg Roach
5871fe542e9SGreg Roach        if ($individual === null && $this->getUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF) !== '') {
5881fe542e9SGreg Roach            $individual = Registry::individualFactory()->make($this->getUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF), $this);
5898586983fSGreg Roach        }
5908f9b0fb2SGreg Roach
5911fe542e9SGreg Roach        if ($individual === null && $this->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF) !== '') {
5921fe542e9SGreg Roach            $individual = Registry::individualFactory()->make($this->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF), $this);
5938586983fSGreg Roach        }
5948f9b0fb2SGreg Roach
595bec87e94SGreg Roach        if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') {
5966b9cb339SGreg Roach            $individual = Registry::individualFactory()->make($this->getPreference('PEDIGREE_ROOT_ID'), $this);
5978586983fSGreg Roach        }
5988f9b0fb2SGreg Roach        if ($individual === null) {
5998f9b0fb2SGreg Roach            $xref = (string) DB::table('individuals')
6008f9b0fb2SGreg Roach                ->where('i_file', '=', $this->id())
6018f9b0fb2SGreg Roach                ->min('i_id');
602769d7d6eSGreg Roach
6036b9cb339SGreg Roach            $individual = Registry::individualFactory()->make($xref, $this);
6045fe1add5SGreg Roach        }
6058f9b0fb2SGreg Roach        if ($individual === null) {
6065fe1add5SGreg Roach            // always return a record
6076b9cb339SGreg Roach            $individual = Registry::individualFactory()->new('I', '0 @I@ INDI', null, $this);
6085fe1add5SGreg Roach        }
6095fe1add5SGreg Roach
6105fe1add5SGreg Roach        return $individual;
6115fe1add5SGreg Roach    }
6121df7ae39SGreg Roach
61385a166d8SGreg Roach    /**
61485a166d8SGreg Roach     * Where do we store our media files.
61585a166d8SGreg Roach     *
616*f7cf8a15SGreg Roach     * @param FilesystemOperator $data_filesystem
617a04bb9a2SGreg Roach     *
618*f7cf8a15SGreg Roach     * @return FilesystemOperator
61985a166d8SGreg Roach     */
620*f7cf8a15SGreg Roach    public function mediaFilesystem(FilesystemOperator $data_filesystem): FilesystemOperator
6211df7ae39SGreg Roach    {
622456d0d35SGreg Roach        $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/');
623a04bb9a2SGreg Roach        $adapter   = new ChrootAdapter($data_filesystem, $media_dir);
624456d0d35SGreg Roach
625456d0d35SGreg Roach        return new Filesystem($adapter);
6261df7ae39SGreg Roach    }
627a25f0a04SGreg Roach}
628