xref: /webtrees/app/Tree.php (revision 3df1e584fbd868b51c2f8559129ab0652c3acaa3)
1a25f0a04SGreg Roach<?php
23976b470SGreg Roach
3a25f0a04SGreg Roach/**
4a25f0a04SGreg Roach * webtrees: online genealogy
58fcd0d32SGreg Roach * Copyright (C) 2019 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
15a25f0a04SGreg Roach * along with this program. If not, see <http://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;
253d7a8a4cSGreg Roachuse Fisharebest\Webtrees\Functions\FunctionsExport;
263d7a8a4cSGreg Roachuse Fisharebest\Webtrees\Functions\FunctionsImport;
275afbc57aSGreg Roachuse Fisharebest\Webtrees\Services\TreeService;
2801461f86SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
29a69f5655SGreg Roachuse Illuminate\Database\Query\Expression;
3094026f20SGreg Roachuse Illuminate\Support\Collection;
31bec87e94SGreg Roachuse Illuminate\Support\Str;
32afb591d7SGreg Roachuse InvalidArgumentException;
331df7ae39SGreg Roachuse League\Flysystem\Filesystem;
341df7ae39SGreg Roachuse League\Flysystem\FilesystemInterface;
356ccdf4f0SGreg Roachuse Psr\Http\Message\StreamInterface;
368b67c11aSGreg Roachuse stdClass;
37a25f0a04SGreg Roach
38a25f0a04SGreg Roach/**
3976692c8bSGreg Roach * Provide an interface to the wt_gedcom table.
40a25f0a04SGreg Roach */
41c1010edaSGreg Roachclass Tree
42c1010edaSGreg Roach{
43061b43d7SGreg Roach    private const RESN_PRIVACY = [
44061b43d7SGreg Roach        'none'         => Auth::PRIV_PRIVATE,
45061b43d7SGreg Roach        'privacy'      => Auth::PRIV_USER,
46061b43d7SGreg Roach        'confidential' => Auth::PRIV_NONE,
47061b43d7SGreg Roach        'hidden'       => Auth::PRIV_HIDE,
48061b43d7SGreg Roach    ];
49*3df1e584SGreg Roach
506ccdf4f0SGreg Roach    /** @var int The tree's ID number */
516ccdf4f0SGreg Roach    private $id;
52*3df1e584SGreg Roach
536ccdf4f0SGreg Roach    /** @var string The tree's name */
546ccdf4f0SGreg Roach    private $name;
55*3df1e584SGreg Roach
566ccdf4f0SGreg Roach    /** @var string The tree's title */
576ccdf4f0SGreg Roach    private $title;
58*3df1e584SGreg Roach
596ccdf4f0SGreg Roach    /** @var int[] Default access rules for facts in this tree */
606ccdf4f0SGreg Roach    private $fact_privacy;
61*3df1e584SGreg Roach
626ccdf4f0SGreg Roach    /** @var int[] Default access rules for individuals in this tree */
636ccdf4f0SGreg Roach    private $individual_privacy;
64*3df1e584SGreg Roach
656ccdf4f0SGreg Roach    /** @var integer[][] Default access rules for individual facts in this tree */
666ccdf4f0SGreg Roach    private $individual_fact_privacy;
67*3df1e584SGreg Roach
686ccdf4f0SGreg Roach    /** @var string[] Cached copy of the wt_gedcom_setting table. */
696ccdf4f0SGreg Roach    private $preferences = [];
70*3df1e584SGreg Roach
716ccdf4f0SGreg Roach    /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */
726ccdf4f0SGreg Roach    private $user_preferences = [];
73061b43d7SGreg Roach
74a25f0a04SGreg Roach    /**
75*3df1e584SGreg Roach     * Create a tree object.
76a25f0a04SGreg Roach     *
7772cf66d4SGreg Roach     * @param int    $id
78aa6f03bbSGreg Roach     * @param string $name
79cc13d6d8SGreg Roach     * @param string $title
80a25f0a04SGreg Roach     */
815afbc57aSGreg Roach    public function __construct(int $id, string $name, string $title)
82c1010edaSGreg Roach    {
8372cf66d4SGreg Roach        $this->id                      = $id;
84aa6f03bbSGreg Roach        $this->name                    = $name;
85cc13d6d8SGreg Roach        $this->title                   = $title;
8613abd6f3SGreg Roach        $this->fact_privacy            = [];
8713abd6f3SGreg Roach        $this->individual_privacy      = [];
8813abd6f3SGreg Roach        $this->individual_fact_privacy = [];
89518bbdc1SGreg Roach
90518bbdc1SGreg Roach        // Load the privacy settings for this tree
91061b43d7SGreg Roach        $rows = DB::table('default_resn')
92061b43d7SGreg Roach            ->where('gedcom_id', '=', $this->id)
93061b43d7SGreg Roach            ->get();
94518bbdc1SGreg Roach
95518bbdc1SGreg Roach        foreach ($rows as $row) {
96061b43d7SGreg Roach            // Convert GEDCOM privacy restriction to a webtrees access level.
97061b43d7SGreg Roach            $row->resn = self::RESN_PRIVACY[$row->resn];
98061b43d7SGreg Roach
99518bbdc1SGreg Roach            if ($row->xref !== null) {
100518bbdc1SGreg Roach                if ($row->tag_type !== null) {
101518bbdc1SGreg Roach                    $this->individual_fact_privacy[$row->xref][$row->tag_type] = (int) $row->resn;
102518bbdc1SGreg Roach                } else {
103518bbdc1SGreg Roach                    $this->individual_privacy[$row->xref] = (int) $row->resn;
104518bbdc1SGreg Roach                }
105518bbdc1SGreg Roach            } else {
106518bbdc1SGreg Roach                $this->fact_privacy[$row->tag_type] = (int) $row->resn;
107518bbdc1SGreg Roach            }
108518bbdc1SGreg Roach        }
109a25f0a04SGreg Roach    }
110a25f0a04SGreg Roach
111a25f0a04SGreg Roach    /**
1125afbc57aSGreg Roach     * A closure which will create a record from a database row.
1135afbc57aSGreg Roach     *
1145afbc57aSGreg Roach     * @return Closure
1155afbc57aSGreg Roach     */
1165afbc57aSGreg Roach    public static function rowMapper(): Closure
1175afbc57aSGreg Roach    {
1185afbc57aSGreg Roach        return static function (stdClass $row): Tree {
1195afbc57aSGreg Roach            return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title);
1205afbc57aSGreg Roach        };
1215afbc57aSGreg Roach    }
1225afbc57aSGreg Roach
1235afbc57aSGreg Roach    /**
1246ccdf4f0SGreg Roach     * Find the tree with a specific ID.
125a25f0a04SGreg Roach     *
1266ccdf4f0SGreg Roach     * @param int $tree_id
1276ccdf4f0SGreg Roach     *
1286ccdf4f0SGreg Roach     * @return Tree
129a25f0a04SGreg Roach     */
1306ccdf4f0SGreg Roach    public static function findById(int $tree_id): Tree
131c1010edaSGreg Roach    {
1326ccdf4f0SGreg Roach        return self::getAll()[$tree_id];
133a25f0a04SGreg Roach    }
134a25f0a04SGreg Roach
135a25f0a04SGreg Roach    /**
1366ccdf4f0SGreg Roach     * Fetch all the trees that we have permission to access.
137a25f0a04SGreg Roach     *
1386ccdf4f0SGreg Roach     * @return Tree[]
1395afbc57aSGreg Roach     * @deprecated
140a25f0a04SGreg Roach     */
1416ccdf4f0SGreg Roach    public static function getAll(): array
142c1010edaSGreg Roach    {
143*3df1e584SGreg Roach        return (new TreeService())->all()->all();
144a25f0a04SGreg Roach    }
145a25f0a04SGreg Roach
146a25f0a04SGreg Roach    /**
1478b67c11aSGreg Roach     * All the trees that we have permission to access.
148a25f0a04SGreg Roach     *
14954c7f8dfSGreg Roach     * @return Collection
1505afbc57aSGreg Roach     * @deprecated
151a25f0a04SGreg Roach     */
1528b67c11aSGreg Roach    public static function all(): Collection
153c1010edaSGreg Roach    {
1545afbc57aSGreg Roach        return (new TreeService())->all();
155a25f0a04SGreg Roach    }
1568b67c11aSGreg Roach
1578b67c11aSGreg Roach    /**
158a25f0a04SGreg Roach     * Create arguments to select_edit_control()
159a25f0a04SGreg Roach     * Note - these will be escaped later
160a25f0a04SGreg Roach     *
161a25f0a04SGreg Roach     * @return string[]
162a25f0a04SGreg Roach     */
163771ae10aSGreg Roach    public static function getIdList(): array
164c1010edaSGreg Roach    {
16513abd6f3SGreg Roach        $list = [];
166a25f0a04SGreg Roach        foreach (self::getAll() as $tree) {
16772cf66d4SGreg Roach            $list[$tree->id] = $tree->title;
168a25f0a04SGreg Roach        }
169a25f0a04SGreg Roach
170a25f0a04SGreg Roach        return $list;
171a25f0a04SGreg Roach    }
172a25f0a04SGreg Roach
173a25f0a04SGreg Roach    /**
174a25f0a04SGreg Roach     * Create arguments to select_edit_control()
175a25f0a04SGreg Roach     * Note - these will be escaped later
176a25f0a04SGreg Roach     *
177a25f0a04SGreg Roach     * @return string[]
178a25f0a04SGreg Roach     */
179771ae10aSGreg Roach    public static function getNameList(): array
180c1010edaSGreg Roach    {
18113abd6f3SGreg Roach        $list = [];
182a25f0a04SGreg Roach        foreach (self::getAll() as $tree) {
183a25f0a04SGreg Roach            $list[$tree->name] = $tree->title;
184a25f0a04SGreg Roach        }
185a25f0a04SGreg Roach
186a25f0a04SGreg Roach        return $list;
187a25f0a04SGreg Roach    }
188a25f0a04SGreg Roach
189a25f0a04SGreg Roach    /**
190a25f0a04SGreg Roach     * Create a new tree
191a25f0a04SGreg Roach     *
192a25f0a04SGreg Roach     * @param string $tree_name
193a25f0a04SGreg Roach     * @param string $tree_title
194a25f0a04SGreg Roach     *
195a25f0a04SGreg Roach     * @return Tree
1965afbc57aSGreg Roach     * @deprecated
197a25f0a04SGreg Roach     */
198771ae10aSGreg Roach    public static function create(string $tree_name, string $tree_title): Tree
199c1010edaSGreg Roach    {
2005afbc57aSGreg Roach        return (new TreeService())->create($tree_name, $tree_title);
201a25f0a04SGreg Roach    }
202a25f0a04SGreg Roach
203a25f0a04SGreg Roach    /**
2046ccdf4f0SGreg Roach     * Find the tree with a specific name.
2056ccdf4f0SGreg Roach     *
2065afbc57aSGreg Roach     * @param string $name
2076ccdf4f0SGreg Roach     *
2086ccdf4f0SGreg Roach     * @return Tree|null
2095afbc57aSGreg Roach     * @deprecated
2106ccdf4f0SGreg Roach     */
2115afbc57aSGreg Roach    public static function findByName($name): ?Tree
2126ccdf4f0SGreg Roach    {
2136ccdf4f0SGreg Roach        foreach (self::getAll() as $tree) {
2145afbc57aSGreg Roach            if ($tree->name === $name) {
2156ccdf4f0SGreg Roach                return $tree;
2166ccdf4f0SGreg Roach            }
2176ccdf4f0SGreg Roach        }
2186ccdf4f0SGreg Roach
2196ccdf4f0SGreg Roach        return null;
2206ccdf4f0SGreg Roach    }
2216ccdf4f0SGreg Roach
2226ccdf4f0SGreg Roach    /**
2236ccdf4f0SGreg Roach     * Set the tree’s configuration settings.
2246ccdf4f0SGreg Roach     *
2256ccdf4f0SGreg Roach     * @param string $setting_name
2266ccdf4f0SGreg Roach     * @param string $setting_value
2276ccdf4f0SGreg Roach     *
2286ccdf4f0SGreg Roach     * @return $this
2296ccdf4f0SGreg Roach     */
2306ccdf4f0SGreg Roach    public function setPreference(string $setting_name, string $setting_value): Tree
2316ccdf4f0SGreg Roach    {
2326ccdf4f0SGreg Roach        if ($setting_value !== $this->getPreference($setting_name)) {
2336ccdf4f0SGreg Roach            DB::table('gedcom_setting')->updateOrInsert([
2346ccdf4f0SGreg Roach                'gedcom_id'    => $this->id,
2356ccdf4f0SGreg Roach                'setting_name' => $setting_name,
2366ccdf4f0SGreg Roach            ], [
2376ccdf4f0SGreg Roach                'setting_value' => $setting_value,
2386ccdf4f0SGreg Roach            ]);
2396ccdf4f0SGreg Roach
2406ccdf4f0SGreg Roach            $this->preferences[$setting_name] = $setting_value;
2416ccdf4f0SGreg Roach
2426ccdf4f0SGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this);
2436ccdf4f0SGreg Roach        }
2446ccdf4f0SGreg Roach
2456ccdf4f0SGreg Roach        return $this;
2466ccdf4f0SGreg Roach    }
2476ccdf4f0SGreg Roach
2486ccdf4f0SGreg Roach    /**
2496ccdf4f0SGreg Roach     * Get the tree’s configuration settings.
2506ccdf4f0SGreg Roach     *
2516ccdf4f0SGreg Roach     * @param string $setting_name
2526ccdf4f0SGreg Roach     * @param string $default
2536ccdf4f0SGreg Roach     *
2546ccdf4f0SGreg Roach     * @return string
2556ccdf4f0SGreg Roach     */
2566ccdf4f0SGreg Roach    public function getPreference(string $setting_name, string $default = ''): string
2576ccdf4f0SGreg Roach    {
2586ccdf4f0SGreg Roach        if (empty($this->preferences)) {
2596ccdf4f0SGreg Roach            $this->preferences = DB::table('gedcom_setting')
2606ccdf4f0SGreg Roach                ->where('gedcom_id', '=', $this->id)
2616ccdf4f0SGreg Roach                ->pluck('setting_value', 'setting_name')
2626ccdf4f0SGreg Roach                ->all();
2636ccdf4f0SGreg Roach        }
2646ccdf4f0SGreg Roach
2656ccdf4f0SGreg Roach        return $this->preferences[$setting_name] ?? $default;
2666ccdf4f0SGreg Roach    }
2676ccdf4f0SGreg Roach
2686ccdf4f0SGreg Roach    /**
2696ccdf4f0SGreg Roach     * The name of this tree
2706ccdf4f0SGreg Roach     *
2716ccdf4f0SGreg Roach     * @return string
2726ccdf4f0SGreg Roach     */
2736ccdf4f0SGreg Roach    public function name(): string
2746ccdf4f0SGreg Roach    {
2756ccdf4f0SGreg Roach        return $this->name;
2766ccdf4f0SGreg Roach    }
2776ccdf4f0SGreg Roach
2786ccdf4f0SGreg Roach    /**
2796ccdf4f0SGreg Roach     * The title of this tree
2806ccdf4f0SGreg Roach     *
2816ccdf4f0SGreg Roach     * @return string
2826ccdf4f0SGreg Roach     */
2836ccdf4f0SGreg Roach    public function title(): string
2846ccdf4f0SGreg Roach    {
2856ccdf4f0SGreg Roach        return $this->title;
2866ccdf4f0SGreg Roach    }
2876ccdf4f0SGreg Roach
2886ccdf4f0SGreg Roach    /**
2896ccdf4f0SGreg Roach     * The fact-level privacy for this tree.
2906ccdf4f0SGreg Roach     *
2916ccdf4f0SGreg Roach     * @return int[]
2926ccdf4f0SGreg Roach     */
2936ccdf4f0SGreg Roach    public function getFactPrivacy(): array
2946ccdf4f0SGreg Roach    {
2956ccdf4f0SGreg Roach        return $this->fact_privacy;
2966ccdf4f0SGreg Roach    }
2976ccdf4f0SGreg Roach
2986ccdf4f0SGreg Roach    /**
2996ccdf4f0SGreg Roach     * The individual-level privacy for this tree.
3006ccdf4f0SGreg Roach     *
3016ccdf4f0SGreg Roach     * @return int[]
3026ccdf4f0SGreg Roach     */
3036ccdf4f0SGreg Roach    public function getIndividualPrivacy(): array
3046ccdf4f0SGreg Roach    {
3056ccdf4f0SGreg Roach        return $this->individual_privacy;
3066ccdf4f0SGreg Roach    }
3076ccdf4f0SGreg Roach
3086ccdf4f0SGreg Roach    /**
3096ccdf4f0SGreg Roach     * The individual-fact-level privacy for this tree.
3106ccdf4f0SGreg Roach     *
3116ccdf4f0SGreg Roach     * @return int[][]
3126ccdf4f0SGreg Roach     */
3136ccdf4f0SGreg Roach    public function getIndividualFactPrivacy(): array
3146ccdf4f0SGreg Roach    {
3156ccdf4f0SGreg Roach        return $this->individual_fact_privacy;
3166ccdf4f0SGreg Roach    }
3176ccdf4f0SGreg Roach
3186ccdf4f0SGreg Roach    /**
3196ccdf4f0SGreg Roach     * Set the tree’s user-configuration settings.
3206ccdf4f0SGreg Roach     *
3216ccdf4f0SGreg Roach     * @param UserInterface $user
3226ccdf4f0SGreg Roach     * @param string        $setting_name
3236ccdf4f0SGreg Roach     * @param string        $setting_value
3246ccdf4f0SGreg Roach     *
3256ccdf4f0SGreg Roach     * @return $this
3266ccdf4f0SGreg Roach     */
3276ccdf4f0SGreg Roach    public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree
3286ccdf4f0SGreg Roach    {
3296ccdf4f0SGreg Roach        if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
3306ccdf4f0SGreg Roach            // Update the database
3316ccdf4f0SGreg Roach            DB::table('user_gedcom_setting')->updateOrInsert([
3326ccdf4f0SGreg Roach                'gedcom_id'    => $this->id(),
3336ccdf4f0SGreg Roach                'user_id'      => $user->id(),
3346ccdf4f0SGreg Roach                'setting_name' => $setting_name,
3356ccdf4f0SGreg Roach            ], [
3366ccdf4f0SGreg Roach                'setting_value' => $setting_value,
3376ccdf4f0SGreg Roach            ]);
3386ccdf4f0SGreg Roach
3396ccdf4f0SGreg Roach            // Update the cache
3406ccdf4f0SGreg Roach            $this->user_preferences[$user->id()][$setting_name] = $setting_value;
3416ccdf4f0SGreg Roach            // Audit log of changes
3426ccdf4f0SGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this);
3436ccdf4f0SGreg Roach        }
3446ccdf4f0SGreg Roach
3456ccdf4f0SGreg Roach        return $this;
3466ccdf4f0SGreg Roach    }
3476ccdf4f0SGreg Roach
3486ccdf4f0SGreg Roach    /**
3496ccdf4f0SGreg Roach     * Get the tree’s user-configuration settings.
3506ccdf4f0SGreg Roach     *
3516ccdf4f0SGreg Roach     * @param UserInterface $user
3526ccdf4f0SGreg Roach     * @param string        $setting_name
3536ccdf4f0SGreg Roach     * @param string        $default
3546ccdf4f0SGreg Roach     *
3556ccdf4f0SGreg Roach     * @return string
3566ccdf4f0SGreg Roach     */
3576ccdf4f0SGreg Roach    public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string
3586ccdf4f0SGreg Roach    {
3596ccdf4f0SGreg Roach        // There are lots of settings, and we need to fetch lots of them on every page
3606ccdf4f0SGreg Roach        // so it is quicker to fetch them all in one go.
3616ccdf4f0SGreg Roach        if (!array_key_exists($user->id(), $this->user_preferences)) {
3626ccdf4f0SGreg Roach            $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting')
3636ccdf4f0SGreg Roach                ->where('user_id', '=', $user->id())
3646ccdf4f0SGreg Roach                ->where('gedcom_id', '=', $this->id)
3656ccdf4f0SGreg Roach                ->pluck('setting_value', 'setting_name')
3666ccdf4f0SGreg Roach                ->all();
3676ccdf4f0SGreg Roach        }
3686ccdf4f0SGreg Roach
3696ccdf4f0SGreg Roach        return $this->user_preferences[$user->id()][$setting_name] ?? $default;
3706ccdf4f0SGreg Roach    }
3716ccdf4f0SGreg Roach
3726ccdf4f0SGreg Roach    /**
3736ccdf4f0SGreg Roach     * The ID of this tree
3746ccdf4f0SGreg Roach     *
3756ccdf4f0SGreg Roach     * @return int
3766ccdf4f0SGreg Roach     */
3776ccdf4f0SGreg Roach    public function id(): int
3786ccdf4f0SGreg Roach    {
3796ccdf4f0SGreg Roach        return $this->id;
3806ccdf4f0SGreg Roach    }
3816ccdf4f0SGreg Roach
3826ccdf4f0SGreg Roach    /**
3836ccdf4f0SGreg Roach     * Can a user accept changes for this tree?
3846ccdf4f0SGreg Roach     *
3856ccdf4f0SGreg Roach     * @param UserInterface $user
3866ccdf4f0SGreg Roach     *
3876ccdf4f0SGreg Roach     * @return bool
3886ccdf4f0SGreg Roach     */
3896ccdf4f0SGreg Roach    public function canAcceptChanges(UserInterface $user): bool
3906ccdf4f0SGreg Roach    {
3916ccdf4f0SGreg Roach        return Auth::isModerator($this, $user);
3926ccdf4f0SGreg Roach    }
3936ccdf4f0SGreg Roach
3946ccdf4f0SGreg Roach    /**
395b78374c5SGreg Roach     * Are there any pending edits for this tree, than need reviewing by a moderator.
396b78374c5SGreg Roach     *
397b78374c5SGreg Roach     * @return bool
398b78374c5SGreg Roach     */
399771ae10aSGreg Roach    public function hasPendingEdit(): bool
400c1010edaSGreg Roach    {
40115a3f100SGreg Roach        return DB::table('change')
40215a3f100SGreg Roach            ->where('gedcom_id', '=', $this->id)
40315a3f100SGreg Roach            ->where('status', '=', 'pending')
40415a3f100SGreg Roach            ->exists();
405b78374c5SGreg Roach    }
406b78374c5SGreg Roach
407b78374c5SGreg Roach    /**
4086ccdf4f0SGreg Roach     * Delete everything relating to a tree
4096ccdf4f0SGreg Roach     *
4106ccdf4f0SGreg Roach     * @return void
4116ccdf4f0SGreg Roach     */
4126ccdf4f0SGreg Roach    public function delete(): void
4136ccdf4f0SGreg Roach    {
4146ccdf4f0SGreg Roach        // If this is the default tree, then unset it
4156ccdf4f0SGreg Roach        if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) {
4166ccdf4f0SGreg Roach            Site::setPreference('DEFAULT_GEDCOM', '');
4176ccdf4f0SGreg Roach        }
4186ccdf4f0SGreg Roach
4196ccdf4f0SGreg Roach        $this->deleteGenealogyData(false);
4206ccdf4f0SGreg Roach
4216ccdf4f0SGreg Roach        DB::table('block_setting')
4226ccdf4f0SGreg Roach            ->join('block', 'block.block_id', '=', 'block_setting.block_id')
4236ccdf4f0SGreg Roach            ->where('gedcom_id', '=', $this->id)
4246ccdf4f0SGreg Roach            ->delete();
4256ccdf4f0SGreg Roach        DB::table('block')->where('gedcom_id', '=', $this->id)->delete();
4266ccdf4f0SGreg Roach        DB::table('user_gedcom_setting')->where('gedcom_id', '=', $this->id)->delete();
4276ccdf4f0SGreg Roach        DB::table('gedcom_setting')->where('gedcom_id', '=', $this->id)->delete();
4286ccdf4f0SGreg Roach        DB::table('module_privacy')->where('gedcom_id', '=', $this->id)->delete();
4296ccdf4f0SGreg Roach        DB::table('hit_counter')->where('gedcom_id', '=', $this->id)->delete();
4306ccdf4f0SGreg Roach        DB::table('default_resn')->where('gedcom_id', '=', $this->id)->delete();
4316ccdf4f0SGreg Roach        DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete();
4326ccdf4f0SGreg Roach        DB::table('log')->where('gedcom_id', '=', $this->id)->delete();
4336ccdf4f0SGreg Roach        DB::table('gedcom')->where('gedcom_id', '=', $this->id)->delete();
4346ccdf4f0SGreg Roach    }
4356ccdf4f0SGreg Roach
4366ccdf4f0SGreg Roach    /**
437a25f0a04SGreg Roach     * Delete all the genealogy data from a tree - in preparation for importing
438a25f0a04SGreg Roach     * new data. Optionally retain the media data, for when the user has been
439a25f0a04SGreg Roach     * editing their data offline using an application which deletes (or does not
440a25f0a04SGreg Roach     * support) media data.
441a25f0a04SGreg Roach     *
442a25f0a04SGreg Roach     * @param bool $keep_media
443b7e60af1SGreg Roach     *
444b7e60af1SGreg Roach     * @return void
445a25f0a04SGreg Roach     */
446e364afe4SGreg Roach    public function deleteGenealogyData(bool $keep_media): void
447c1010edaSGreg Roach    {
4481ad2dde6SGreg Roach        DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete();
4491ad2dde6SGreg Roach        DB::table('individuals')->where('i_file', '=', $this->id)->delete();
4501ad2dde6SGreg Roach        DB::table('families')->where('f_file', '=', $this->id)->delete();
4511ad2dde6SGreg Roach        DB::table('sources')->where('s_file', '=', $this->id)->delete();
4521ad2dde6SGreg Roach        DB::table('other')->where('o_file', '=', $this->id)->delete();
4531ad2dde6SGreg Roach        DB::table('places')->where('p_file', '=', $this->id)->delete();
4541ad2dde6SGreg Roach        DB::table('placelinks')->where('pl_file', '=', $this->id)->delete();
4551ad2dde6SGreg Roach        DB::table('name')->where('n_file', '=', $this->id)->delete();
4561ad2dde6SGreg Roach        DB::table('dates')->where('d_file', '=', $this->id)->delete();
4571ad2dde6SGreg Roach        DB::table('change')->where('gedcom_id', '=', $this->id)->delete();
458a25f0a04SGreg Roach
459a25f0a04SGreg Roach        if ($keep_media) {
4601ad2dde6SGreg Roach            DB::table('link')->where('l_file', '=', $this->id)
4611ad2dde6SGreg Roach                ->where('l_type', '<>', 'OBJE')
4621ad2dde6SGreg Roach                ->delete();
463a25f0a04SGreg Roach        } else {
4641ad2dde6SGreg Roach            DB::table('link')->where('l_file', '=', $this->id)->delete();
4651ad2dde6SGreg Roach            DB::table('media_file')->where('m_file', '=', $this->id)->delete();
4661ad2dde6SGreg Roach            DB::table('media')->where('m_file', '=', $this->id)->delete();
467a25f0a04SGreg Roach        }
468a25f0a04SGreg Roach    }
469a25f0a04SGreg Roach
470a25f0a04SGreg Roach    /**
471a25f0a04SGreg Roach     * Export the tree to a GEDCOM file
472a25f0a04SGreg Roach     *
4735792757eSGreg Roach     * @param resource $stream
474b7e60af1SGreg Roach     *
475b7e60af1SGreg Roach     * @return void
476a25f0a04SGreg Roach     */
477425af8b9SGreg Roach    public function exportGedcom($stream): void
478c1010edaSGreg Roach    {
479a3d8780cSGreg Roach        $buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this, 'UTF-8'));
48094026f20SGreg Roach
48194026f20SGreg Roach        $union_families = DB::table('families')
48294026f20SGreg Roach            ->where('f_file', '=', $this->id)
483a69f5655SGreg Roach            ->select(['f_gedcom AS gedcom', 'f_id AS xref', new Expression('LENGTH(f_id) AS len'), new Expression('2 AS n')]);
48494026f20SGreg Roach
48594026f20SGreg Roach        $union_sources = DB::table('sources')
48694026f20SGreg Roach            ->where('s_file', '=', $this->id)
487a69f5655SGreg Roach            ->select(['s_gedcom AS gedcom', 's_id AS xref', new Expression('LENGTH(s_id) AS len'), new Expression('3 AS n')]);
48894026f20SGreg Roach
48994026f20SGreg Roach        $union_other = DB::table('other')
49094026f20SGreg Roach            ->where('o_file', '=', $this->id)
49194026f20SGreg Roach            ->whereNotIn('o_type', ['HEAD', 'TRLR'])
492a69f5655SGreg Roach            ->select(['o_gedcom AS gedcom', 'o_id AS xref', new Expression('LENGTH(o_id) AS len'), new Expression('4 AS n')]);
49394026f20SGreg Roach
49494026f20SGreg Roach        $union_media = DB::table('media')
49594026f20SGreg Roach            ->where('m_file', '=', $this->id)
496a69f5655SGreg Roach            ->select(['m_gedcom AS gedcom', 'm_id AS xref', new Expression('LENGTH(m_id) AS len'), new Expression('5 AS n')]);
49794026f20SGreg Roach
498e5a6b4d4SGreg Roach        DB::table('individuals')
49994026f20SGreg Roach            ->where('i_file', '=', $this->id)
500a69f5655SGreg Roach            ->select(['i_gedcom AS gedcom', 'i_id AS xref', new Expression('LENGTH(i_id) AS len'), new Expression('1 AS n')])
50194026f20SGreg Roach            ->union($union_families)
50294026f20SGreg Roach            ->union($union_sources)
50394026f20SGreg Roach            ->union($union_other)
50494026f20SGreg Roach            ->union($union_media)
50594026f20SGreg Roach            ->orderBy('n')
50694026f20SGreg Roach            ->orderBy('len')
50794026f20SGreg Roach            ->orderBy('xref')
50827825e0aSGreg Roach            ->chunk(1000, static function (Collection $rows) use ($stream, &$buffer): void {
50994026f20SGreg Roach                foreach ($rows as $row) {
5103d7a8a4cSGreg Roach                    $buffer .= FunctionsExport::reformatRecord($row->gedcom);
511a25f0a04SGreg Roach                    if (strlen($buffer) > 65535) {
5125792757eSGreg Roach                        fwrite($stream, $buffer);
513a25f0a04SGreg Roach                        $buffer = '';
514a25f0a04SGreg Roach                    }
515a25f0a04SGreg Roach                }
51694026f20SGreg Roach            });
51794026f20SGreg Roach
5180f471f91SGreg Roach        fwrite($stream, $buffer . '0 TRLR' . Gedcom::EOL);
519a25f0a04SGreg Roach    }
520a25f0a04SGreg Roach
521a25f0a04SGreg Roach    /**
522a25f0a04SGreg Roach     * Import data from a gedcom file into this tree.
523a25f0a04SGreg Roach     *
5246ccdf4f0SGreg Roach     * @param StreamInterface $stream   The GEDCOM file.
525a25f0a04SGreg Roach     * @param string          $filename The preferred filename, for export/download.
526a25f0a04SGreg Roach     *
527b7e60af1SGreg Roach     * @return void
528a25f0a04SGreg Roach     */
5296ccdf4f0SGreg Roach    public function importGedcomFile(StreamInterface $stream, string $filename): void
530c1010edaSGreg Roach    {
531a25f0a04SGreg Roach        // Read the file in blocks of roughly 64K. Ensure that each block
532a25f0a04SGreg Roach        // contains complete gedcom records. This will ensure we don’t split
533a25f0a04SGreg Roach        // multi-byte characters, as well as simplifying the code to import
534a25f0a04SGreg Roach        // each block.
535a25f0a04SGreg Roach
536a25f0a04SGreg Roach        $file_data = '';
537a25f0a04SGreg Roach
538b7e60af1SGreg Roach        $this->deleteGenealogyData((bool) $this->getPreference('keep_media'));
539a25f0a04SGreg Roach        $this->setPreference('gedcom_filename', $filename);
540a25f0a04SGreg Roach        $this->setPreference('imported', '0');
541a25f0a04SGreg Roach
5426ccdf4f0SGreg Roach        while (!$stream->eof()) {
5436ccdf4f0SGreg Roach            $file_data .= $stream->read(65536);
544a25f0a04SGreg Roach            // There is no strrpos() function that searches for substrings :-(
545a25f0a04SGreg Roach            for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) {
546a25f0a04SGreg Roach                if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) {
547a25f0a04SGreg Roach                    // We’ve found the last record boundary in this chunk of data
548a25f0a04SGreg Roach                    break;
549a25f0a04SGreg Roach                }
550a25f0a04SGreg Roach            }
551a25f0a04SGreg Roach            if ($pos) {
5521ad2dde6SGreg Roach                DB::table('gedcom_chunk')->insert([
5531ad2dde6SGreg Roach                    'gedcom_id'  => $this->id,
5541ad2dde6SGreg Roach                    'chunk_data' => substr($file_data, 0, $pos),
555c1010edaSGreg Roach                ]);
5561ad2dde6SGreg Roach
557a25f0a04SGreg Roach                $file_data = substr($file_data, $pos);
558a25f0a04SGreg Roach            }
559a25f0a04SGreg Roach        }
5601ad2dde6SGreg Roach        DB::table('gedcom_chunk')->insert([
5611ad2dde6SGreg Roach            'gedcom_id'  => $this->id,
5621ad2dde6SGreg Roach            'chunk_data' => $file_data,
563c1010edaSGreg Roach        ]);
564a25f0a04SGreg Roach
5656ccdf4f0SGreg Roach        $stream->close();
5666ccdf4f0SGreg Roach    }
5676ccdf4f0SGreg Roach
5686ccdf4f0SGreg Roach    /**
5696ccdf4f0SGreg Roach     * Create a new record from GEDCOM data.
5706ccdf4f0SGreg Roach     *
5716ccdf4f0SGreg Roach     * @param string $gedcom
5726ccdf4f0SGreg Roach     *
5736ccdf4f0SGreg Roach     * @return GedcomRecord|Individual|Family|Note|Source|Repository|Media
5746ccdf4f0SGreg Roach     * @throws InvalidArgumentException
5756ccdf4f0SGreg Roach     */
5766ccdf4f0SGreg Roach    public function createRecord(string $gedcom): GedcomRecord
5776ccdf4f0SGreg Roach    {
5786ccdf4f0SGreg Roach        if (!Str::startsWith($gedcom, '0 @@ ')) {
5796ccdf4f0SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@');
5806ccdf4f0SGreg Roach        }
5816ccdf4f0SGreg Roach
5826ccdf4f0SGreg Roach        $xref   = $this->getNewXref();
5836ccdf4f0SGreg Roach        $gedcom = '0 @' . $xref . '@ ' . Str::after($gedcom, '0 @@ ');
5846ccdf4f0SGreg Roach
5856ccdf4f0SGreg Roach        // Create a change record
5866ccdf4f0SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName();
5876ccdf4f0SGreg Roach
5886ccdf4f0SGreg Roach        // Create a pending change
5896ccdf4f0SGreg Roach        DB::table('change')->insert([
5906ccdf4f0SGreg Roach            'gedcom_id'  => $this->id,
5916ccdf4f0SGreg Roach            'xref'       => $xref,
5926ccdf4f0SGreg Roach            'old_gedcom' => '',
5936ccdf4f0SGreg Roach            'new_gedcom' => $gedcom,
5946ccdf4f0SGreg Roach            'user_id'    => Auth::id(),
5956ccdf4f0SGreg Roach        ]);
5966ccdf4f0SGreg Roach
5976ccdf4f0SGreg Roach        // Accept this pending change
5986ccdf4f0SGreg Roach        if (Auth::user()->getPreference('auto_accept')) {
5996ccdf4f0SGreg Roach            FunctionsImport::acceptAllChanges($xref, $this);
6006ccdf4f0SGreg Roach
6016ccdf4f0SGreg Roach            return new GedcomRecord($xref, $gedcom, null, $this);
6026ccdf4f0SGreg Roach        }
6036ccdf4f0SGreg Roach
6046ccdf4f0SGreg Roach        return GedcomRecord::getInstance($xref, $this, $gedcom);
605a25f0a04SGreg Roach    }
606304f20d5SGreg Roach
607304f20d5SGreg Roach    /**
608b90d8accSGreg Roach     * Generate a new XREF, unique across all family trees
609b90d8accSGreg Roach     *
610b90d8accSGreg Roach     * @return string
611b90d8accSGreg Roach     */
612771ae10aSGreg Roach    public function getNewXref(): string
613c1010edaSGreg Roach    {
614963fbaeeSGreg Roach        // Lock the row, so that only one new XREF may be generated at a time.
615963fbaeeSGreg Roach        DB::table('site_setting')
616963fbaeeSGreg Roach            ->where('setting_name', '=', 'next_xref')
617963fbaeeSGreg Roach            ->lockForUpdate()
618963fbaeeSGreg Roach            ->get();
619963fbaeeSGreg Roach
620a214e186SGreg Roach        $prefix = 'X';
621b90d8accSGreg Roach
622971d66c8SGreg Roach        $increment = 1.0;
623b90d8accSGreg Roach        do {
624963fbaeeSGreg Roach            $num = (int) Site::getPreference('next_xref') + (int) $increment;
625971d66c8SGreg Roach
626971d66c8SGreg Roach            // This exponential increment allows us to scan over large blocks of
627971d66c8SGreg Roach            // existing data in a reasonable time.
628971d66c8SGreg Roach            $increment *= 1.01;
629963fbaeeSGreg Roach
630963fbaeeSGreg Roach            $xref = $prefix . $num;
631963fbaeeSGreg Roach
632963fbaeeSGreg Roach            // Records may already exist with this sequence number.
633963fbaeeSGreg Roach            $already_used =
634963fbaeeSGreg Roach                DB::table('individuals')->where('i_id', '=', $xref)->exists() ||
635963fbaeeSGreg Roach                DB::table('families')->where('f_id', '=', $xref)->exists() ||
636963fbaeeSGreg Roach                DB::table('sources')->where('s_id', '=', $xref)->exists() ||
637963fbaeeSGreg Roach                DB::table('media')->where('m_id', '=', $xref)->exists() ||
638963fbaeeSGreg Roach                DB::table('other')->where('o_id', '=', $xref)->exists() ||
639963fbaeeSGreg Roach                DB::table('change')->where('xref', '=', $xref)->exists();
640963fbaeeSGreg Roach        } while ($already_used);
641963fbaeeSGreg Roach
642963fbaeeSGreg Roach        Site::setPreference('next_xref', (string) $num);
643b90d8accSGreg Roach
644a214e186SGreg Roach        return $xref;
645b90d8accSGreg Roach    }
646b90d8accSGreg Roach
647b90d8accSGreg Roach    /**
648afb591d7SGreg Roach     * Create a new family from GEDCOM data.
649afb591d7SGreg Roach     *
650afb591d7SGreg Roach     * @param string $gedcom
651afb591d7SGreg Roach     *
652afb591d7SGreg Roach     * @return Family
653afb591d7SGreg Roach     * @throws InvalidArgumentException
654afb591d7SGreg Roach     */
655afb591d7SGreg Roach    public function createFamily(string $gedcom): GedcomRecord
656afb591d7SGreg Roach    {
657bec87e94SGreg Roach        if (!Str::startsWith($gedcom, '0 @@ FAM')) {
658afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM');
659afb591d7SGreg Roach        }
660afb591d7SGreg Roach
661afb591d7SGreg Roach        $xref   = $this->getNewXref();
662bec87e94SGreg Roach        $gedcom = '0 @' . $xref . '@ FAM' . Str::after($gedcom, '0 @@ FAM');
663afb591d7SGreg Roach
664afb591d7SGreg Roach        // Create a change record
665e5a6b4d4SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName();
666afb591d7SGreg Roach
667afb591d7SGreg Roach        // Create a pending change
668963fbaeeSGreg Roach        DB::table('change')->insert([
669963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
670963fbaeeSGreg Roach            'xref'       => $xref,
671963fbaeeSGreg Roach            'old_gedcom' => '',
672963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
673963fbaeeSGreg Roach            'user_id'    => Auth::id(),
674afb591d7SGreg Roach        ]);
675304f20d5SGreg Roach
676304f20d5SGreg Roach        // Accept this pending change
677304f20d5SGreg Roach        if (Auth::user()->getPreference('auto_accept')) {
678cc5684fdSGreg Roach            FunctionsImport::acceptAllChanges($xref, $this);
679afb591d7SGreg Roach
680afb591d7SGreg Roach            return new Family($xref, $gedcom, null, $this);
681304f20d5SGreg Roach        }
682afb591d7SGreg Roach
683afb591d7SGreg Roach        return new Family($xref, '', $gedcom, $this);
684afb591d7SGreg Roach    }
685afb591d7SGreg Roach
686afb591d7SGreg Roach    /**
687afb591d7SGreg Roach     * Create a new individual from GEDCOM data.
688afb591d7SGreg Roach     *
689afb591d7SGreg Roach     * @param string $gedcom
690afb591d7SGreg Roach     *
691afb591d7SGreg Roach     * @return Individual
692afb591d7SGreg Roach     * @throws InvalidArgumentException
693afb591d7SGreg Roach     */
694afb591d7SGreg Roach    public function createIndividual(string $gedcom): GedcomRecord
695afb591d7SGreg Roach    {
696bec87e94SGreg Roach        if (!Str::startsWith($gedcom, '0 @@ INDI')) {
697afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI');
698afb591d7SGreg Roach        }
699afb591d7SGreg Roach
700afb591d7SGreg Roach        $xref   = $this->getNewXref();
701bec87e94SGreg Roach        $gedcom = '0 @' . $xref . '@ INDI' . Str::after($gedcom, '0 @@ INDI');
702afb591d7SGreg Roach
703afb591d7SGreg Roach        // Create a change record
704e5a6b4d4SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName();
705afb591d7SGreg Roach
706afb591d7SGreg Roach        // Create a pending change
707963fbaeeSGreg Roach        DB::table('change')->insert([
708963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
709963fbaeeSGreg Roach            'xref'       => $xref,
710963fbaeeSGreg Roach            'old_gedcom' => '',
711963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
712963fbaeeSGreg Roach            'user_id'    => Auth::id(),
713afb591d7SGreg Roach        ]);
714afb591d7SGreg Roach
715afb591d7SGreg Roach        // Accept this pending change
716afb591d7SGreg Roach        if (Auth::user()->getPreference('auto_accept')) {
717afb591d7SGreg Roach            FunctionsImport::acceptAllChanges($xref, $this);
718afb591d7SGreg Roach
719afb591d7SGreg Roach            return new Individual($xref, $gedcom, null, $this);
720afb591d7SGreg Roach        }
721afb591d7SGreg Roach
722afb591d7SGreg Roach        return new Individual($xref, '', $gedcom, $this);
723304f20d5SGreg Roach    }
7248586983fSGreg Roach
7258586983fSGreg Roach    /**
72620b58d20SGreg Roach     * Create a new media object from GEDCOM data.
72720b58d20SGreg Roach     *
72820b58d20SGreg Roach     * @param string $gedcom
72920b58d20SGreg Roach     *
73020b58d20SGreg Roach     * @return Media
73120b58d20SGreg Roach     * @throws InvalidArgumentException
73220b58d20SGreg Roach     */
73320b58d20SGreg Roach    public function createMediaObject(string $gedcom): Media
73420b58d20SGreg Roach    {
735bec87e94SGreg Roach        if (!Str::startsWith($gedcom, '0 @@ OBJE')) {
73620b58d20SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE');
73720b58d20SGreg Roach        }
73820b58d20SGreg Roach
73920b58d20SGreg Roach        $xref   = $this->getNewXref();
740bec87e94SGreg Roach        $gedcom = '0 @' . $xref . '@ OBJE' . Str::after($gedcom, '0 @@ OBJE');
74120b58d20SGreg Roach
74220b58d20SGreg Roach        // Create a change record
743e5a6b4d4SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName();
74420b58d20SGreg Roach
74520b58d20SGreg Roach        // Create a pending change
746963fbaeeSGreg Roach        DB::table('change')->insert([
747963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
748963fbaeeSGreg Roach            'xref'       => $xref,
749963fbaeeSGreg Roach            'old_gedcom' => '',
750963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
751963fbaeeSGreg Roach            'user_id'    => Auth::id(),
75220b58d20SGreg Roach        ]);
75320b58d20SGreg Roach
75420b58d20SGreg Roach        // Accept this pending change
75520b58d20SGreg Roach        if (Auth::user()->getPreference('auto_accept')) {
75620b58d20SGreg Roach            FunctionsImport::acceptAllChanges($xref, $this);
75720b58d20SGreg Roach
75820b58d20SGreg Roach            return new Media($xref, $gedcom, null, $this);
75920b58d20SGreg Roach        }
76020b58d20SGreg Roach
76120b58d20SGreg Roach        return new Media($xref, '', $gedcom, $this);
76220b58d20SGreg Roach    }
76320b58d20SGreg Roach
76420b58d20SGreg Roach    /**
7658586983fSGreg Roach     * What is the most significant individual in this tree.
7668586983fSGreg Roach     *
767e5a6b4d4SGreg Roach     * @param UserInterface $user
7688586983fSGreg Roach     *
7698586983fSGreg Roach     * @return Individual
7708586983fSGreg Roach     */
771e5a6b4d4SGreg Roach    public function significantIndividual(UserInterface $user): Individual
772c1010edaSGreg Roach    {
7738f9b0fb2SGreg Roach        $individual = null;
7748586983fSGreg Roach
7758f9b0fb2SGreg Roach        if ($this->getUserPreference($user, 'rootid') !== '') {
7768586983fSGreg Roach            $individual = Individual::getInstance($this->getUserPreference($user, 'rootid'), $this);
7778586983fSGreg Roach        }
7788f9b0fb2SGreg Roach
7798f9b0fb2SGreg Roach        if ($individual === null && $this->getUserPreference($user, 'gedcomid') !== '') {
7808586983fSGreg Roach            $individual = Individual::getInstance($this->getUserPreference($user, 'gedcomid'), $this);
7818586983fSGreg Roach        }
7828f9b0fb2SGreg Roach
783bec87e94SGreg Roach        if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') {
7848586983fSGreg Roach            $individual = Individual::getInstance($this->getPreference('PEDIGREE_ROOT_ID'), $this);
7858586983fSGreg Roach        }
7868f9b0fb2SGreg Roach        if ($individual === null) {
7878f9b0fb2SGreg Roach            $xref = (string) DB::table('individuals')
7888f9b0fb2SGreg Roach                ->where('i_file', '=', $this->id())
7898f9b0fb2SGreg Roach                ->min('i_id');
790769d7d6eSGreg Roach
791769d7d6eSGreg Roach            $individual = Individual::getInstance($xref, $this);
7925fe1add5SGreg Roach        }
7938f9b0fb2SGreg Roach        if ($individual === null) {
7945fe1add5SGreg Roach            // always return a record
7955fe1add5SGreg Roach            $individual = new Individual('I', '0 @I@ INDI', null, $this);
7965fe1add5SGreg Roach        }
7975fe1add5SGreg Roach
7985fe1add5SGreg Roach        return $individual;
7995fe1add5SGreg Roach    }
8001df7ae39SGreg Roach
80185a166d8SGreg Roach    /**
80285a166d8SGreg Roach     * Where do we store our media files.
80385a166d8SGreg Roach     *
80485a166d8SGreg Roach     * @return FilesystemInterface
80585a166d8SGreg Roach     */
8061df7ae39SGreg Roach    public function mediaFilesystem(): FilesystemInterface
8071df7ae39SGreg Roach    {
808456d0d35SGreg Roach        $media_dir  = $this->getPreference('MEDIA_DIRECTORY', 'media/');
809456d0d35SGreg Roach        $filesystem = app(FilesystemInterface::class);
810456d0d35SGreg Roach        $adapter    = new ChrootAdapter($filesystem, $media_dir);
811456d0d35SGreg Roach
812456d0d35SGreg Roach        return new Filesystem($adapter);
8131df7ae39SGreg Roach    }
814a25f0a04SGreg Roach}
815