xref: /webtrees/app/Tree.php (revision 1fe542e96f8f7eedeebc278fae1e0ab0d9e74d95)
1a25f0a04SGreg Roach<?php
23976b470SGreg Roach
3a25f0a04SGreg Roach/**
4a25f0a04SGreg Roach * webtrees: online genealogy
5*1fe542e9SGreg 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
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;
2569c05a6eSGreg Roachuse Fisharebest\Webtrees\Services\GedcomExportService;
2622e73debSGreg Roachuse Fisharebest\Webtrees\Services\PendingChangesService;
2701461f86SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
28afb591d7SGreg Roachuse InvalidArgumentException;
291df7ae39SGreg Roachuse League\Flysystem\Filesystem;
301df7ae39SGreg Roachuse League\Flysystem\FilesystemInterface;
316ccdf4f0SGreg Roachuse Psr\Http\Message\StreamInterface;
328b67c11aSGreg Roachuse stdClass;
33a25f0a04SGreg Roach
341e653452SGreg Roachuse function app;
35dec352c1SGreg Roachuse function array_key_exists;
3653432476SGreg Roachuse function date;
37dec352c1SGreg Roachuse function str_starts_with;
38dec352c1SGreg Roachuse function strlen;
3953432476SGreg Roachuse function strtoupper;
40dec352c1SGreg Roachuse function substr;
41dec352c1SGreg Roachuse function substr_replace;
421e653452SGreg Roach
43a25f0a04SGreg Roach/**
4476692c8bSGreg Roach * Provide an interface to the wt_gedcom table.
45a25f0a04SGreg Roach */
46c1010edaSGreg Roachclass Tree
47c1010edaSGreg Roach{
48061b43d7SGreg Roach    private const RESN_PRIVACY = [
49061b43d7SGreg Roach        'none'         => Auth::PRIV_PRIVATE,
50061b43d7SGreg Roach        'privacy'      => Auth::PRIV_USER,
51061b43d7SGreg Roach        'confidential' => Auth::PRIV_NONE,
52061b43d7SGreg Roach        'hidden'       => Auth::PRIV_HIDE,
53061b43d7SGreg Roach    ];
543df1e584SGreg Roach
556ccdf4f0SGreg Roach    /** @var int The tree's ID number */
566ccdf4f0SGreg Roach    private $id;
573df1e584SGreg Roach
586ccdf4f0SGreg Roach    /** @var string The tree's name */
596ccdf4f0SGreg Roach    private $name;
603df1e584SGreg Roach
616ccdf4f0SGreg Roach    /** @var string The tree's title */
626ccdf4f0SGreg Roach    private $title;
633df1e584SGreg Roach
646ccdf4f0SGreg Roach    /** @var int[] Default access rules for facts in this tree */
656ccdf4f0SGreg Roach    private $fact_privacy;
663df1e584SGreg Roach
676ccdf4f0SGreg Roach    /** @var int[] Default access rules for individuals in this tree */
686ccdf4f0SGreg Roach    private $individual_privacy;
693df1e584SGreg Roach
706ccdf4f0SGreg Roach    /** @var integer[][] Default access rules for individual facts in this tree */
716ccdf4f0SGreg Roach    private $individual_fact_privacy;
723df1e584SGreg Roach
736ccdf4f0SGreg Roach    /** @var string[] Cached copy of the wt_gedcom_setting table. */
746ccdf4f0SGreg Roach    private $preferences = [];
753df1e584SGreg Roach
766ccdf4f0SGreg Roach    /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */
776ccdf4f0SGreg Roach    private $user_preferences = [];
78061b43d7SGreg Roach
79a25f0a04SGreg Roach    /**
803df1e584SGreg Roach     * Create a tree object.
81a25f0a04SGreg Roach     *
8272cf66d4SGreg Roach     * @param int    $id
83aa6f03bbSGreg Roach     * @param string $name
84cc13d6d8SGreg Roach     * @param string $title
85a25f0a04SGreg Roach     */
865afbc57aSGreg Roach    public function __construct(int $id, string $name, string $title)
87c1010edaSGreg Roach    {
8872cf66d4SGreg Roach        $this->id                      = $id;
89aa6f03bbSGreg Roach        $this->name                    = $name;
90cc13d6d8SGreg Roach        $this->title                   = $title;
9113abd6f3SGreg Roach        $this->fact_privacy            = [];
9213abd6f3SGreg Roach        $this->individual_privacy      = [];
9313abd6f3SGreg Roach        $this->individual_fact_privacy = [];
94518bbdc1SGreg Roach
95518bbdc1SGreg Roach        // Load the privacy settings for this tree
96061b43d7SGreg Roach        $rows = DB::table('default_resn')
97061b43d7SGreg Roach            ->where('gedcom_id', '=', $this->id)
98061b43d7SGreg Roach            ->get();
99518bbdc1SGreg Roach
100518bbdc1SGreg Roach        foreach ($rows as $row) {
101061b43d7SGreg Roach            // Convert GEDCOM privacy restriction to a webtrees access level.
102061b43d7SGreg Roach            $row->resn = self::RESN_PRIVACY[$row->resn];
103061b43d7SGreg Roach
104518bbdc1SGreg Roach            if ($row->xref !== null) {
105518bbdc1SGreg Roach                if ($row->tag_type !== null) {
106b262b3d3SGreg Roach                    $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn;
107518bbdc1SGreg Roach                } else {
108b262b3d3SGreg Roach                    $this->individual_privacy[$row->xref] = $row->resn;
109518bbdc1SGreg Roach                }
110518bbdc1SGreg Roach            } else {
111b262b3d3SGreg Roach                $this->fact_privacy[$row->tag_type] = $row->resn;
112518bbdc1SGreg Roach            }
113518bbdc1SGreg Roach        }
114a25f0a04SGreg Roach    }
115a25f0a04SGreg Roach
116a25f0a04SGreg Roach    /**
1175afbc57aSGreg Roach     * A closure which will create a record from a database row.
1185afbc57aSGreg Roach     *
1195afbc57aSGreg Roach     * @return Closure
1205afbc57aSGreg Roach     */
1215afbc57aSGreg Roach    public static function rowMapper(): Closure
1225afbc57aSGreg Roach    {
1235afbc57aSGreg Roach        return static function (stdClass $row): Tree {
1245afbc57aSGreg Roach            return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title);
1255afbc57aSGreg Roach        };
1265afbc57aSGreg Roach    }
1275afbc57aSGreg Roach
1285afbc57aSGreg Roach    /**
1296ccdf4f0SGreg Roach     * Set the tree’s configuration settings.
1306ccdf4f0SGreg Roach     *
1316ccdf4f0SGreg Roach     * @param string $setting_name
1326ccdf4f0SGreg Roach     * @param string $setting_value
1336ccdf4f0SGreg Roach     *
1346ccdf4f0SGreg Roach     * @return $this
1356ccdf4f0SGreg Roach     */
1366ccdf4f0SGreg Roach    public function setPreference(string $setting_name, string $setting_value): Tree
1376ccdf4f0SGreg Roach    {
1386ccdf4f0SGreg Roach        if ($setting_value !== $this->getPreference($setting_name)) {
1396ccdf4f0SGreg Roach            DB::table('gedcom_setting')->updateOrInsert([
1406ccdf4f0SGreg Roach                'gedcom_id'    => $this->id,
1416ccdf4f0SGreg Roach                'setting_name' => $setting_name,
1426ccdf4f0SGreg Roach            ], [
1436ccdf4f0SGreg Roach                'setting_value' => $setting_value,
1446ccdf4f0SGreg Roach            ]);
1456ccdf4f0SGreg Roach
1466ccdf4f0SGreg Roach            $this->preferences[$setting_name] = $setting_value;
1476ccdf4f0SGreg Roach
1486ccdf4f0SGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this);
1496ccdf4f0SGreg Roach        }
1506ccdf4f0SGreg Roach
1516ccdf4f0SGreg Roach        return $this;
1526ccdf4f0SGreg Roach    }
1536ccdf4f0SGreg Roach
1546ccdf4f0SGreg Roach    /**
1556ccdf4f0SGreg Roach     * Get the tree’s configuration settings.
1566ccdf4f0SGreg Roach     *
1576ccdf4f0SGreg Roach     * @param string $setting_name
1586ccdf4f0SGreg Roach     * @param string $default
1596ccdf4f0SGreg Roach     *
1606ccdf4f0SGreg Roach     * @return string
1616ccdf4f0SGreg Roach     */
1626ccdf4f0SGreg Roach    public function getPreference(string $setting_name, string $default = ''): string
1636ccdf4f0SGreg Roach    {
16454c1ab5eSGreg Roach        if ($this->preferences === []) {
1656ccdf4f0SGreg Roach            $this->preferences = DB::table('gedcom_setting')
1666ccdf4f0SGreg Roach                ->where('gedcom_id', '=', $this->id)
1676ccdf4f0SGreg Roach                ->pluck('setting_value', 'setting_name')
1686ccdf4f0SGreg Roach                ->all();
1696ccdf4f0SGreg Roach        }
1706ccdf4f0SGreg Roach
1716ccdf4f0SGreg Roach        return $this->preferences[$setting_name] ?? $default;
1726ccdf4f0SGreg Roach    }
1736ccdf4f0SGreg Roach
1746ccdf4f0SGreg Roach    /**
1756ccdf4f0SGreg Roach     * The name of this tree
1766ccdf4f0SGreg Roach     *
1776ccdf4f0SGreg Roach     * @return string
1786ccdf4f0SGreg Roach     */
1796ccdf4f0SGreg Roach    public function name(): string
1806ccdf4f0SGreg Roach    {
1816ccdf4f0SGreg Roach        return $this->name;
1826ccdf4f0SGreg Roach    }
1836ccdf4f0SGreg Roach
1846ccdf4f0SGreg Roach    /**
1856ccdf4f0SGreg Roach     * The title of this tree
1866ccdf4f0SGreg Roach     *
1876ccdf4f0SGreg Roach     * @return string
1886ccdf4f0SGreg Roach     */
1896ccdf4f0SGreg Roach    public function title(): string
1906ccdf4f0SGreg Roach    {
1916ccdf4f0SGreg Roach        return $this->title;
1926ccdf4f0SGreg Roach    }
1936ccdf4f0SGreg Roach
1946ccdf4f0SGreg Roach    /**
1956ccdf4f0SGreg Roach     * The fact-level privacy for this tree.
1966ccdf4f0SGreg Roach     *
1976ccdf4f0SGreg Roach     * @return int[]
1986ccdf4f0SGreg Roach     */
1996ccdf4f0SGreg Roach    public function getFactPrivacy(): array
2006ccdf4f0SGreg Roach    {
2016ccdf4f0SGreg Roach        return $this->fact_privacy;
2026ccdf4f0SGreg Roach    }
2036ccdf4f0SGreg Roach
2046ccdf4f0SGreg Roach    /**
2056ccdf4f0SGreg Roach     * The individual-level privacy for this tree.
2066ccdf4f0SGreg Roach     *
2076ccdf4f0SGreg Roach     * @return int[]
2086ccdf4f0SGreg Roach     */
2096ccdf4f0SGreg Roach    public function getIndividualPrivacy(): array
2106ccdf4f0SGreg Roach    {
2116ccdf4f0SGreg Roach        return $this->individual_privacy;
2126ccdf4f0SGreg Roach    }
2136ccdf4f0SGreg Roach
2146ccdf4f0SGreg Roach    /**
2156ccdf4f0SGreg Roach     * The individual-fact-level privacy for this tree.
2166ccdf4f0SGreg Roach     *
2176ccdf4f0SGreg Roach     * @return int[][]
2186ccdf4f0SGreg Roach     */
2196ccdf4f0SGreg Roach    public function getIndividualFactPrivacy(): array
2206ccdf4f0SGreg Roach    {
2216ccdf4f0SGreg Roach        return $this->individual_fact_privacy;
2226ccdf4f0SGreg Roach    }
2236ccdf4f0SGreg Roach
2246ccdf4f0SGreg Roach    /**
2256ccdf4f0SGreg Roach     * Set the tree’s user-configuration settings.
2266ccdf4f0SGreg Roach     *
2276ccdf4f0SGreg Roach     * @param UserInterface $user
2286ccdf4f0SGreg Roach     * @param string        $setting_name
2296ccdf4f0SGreg Roach     * @param string        $setting_value
2306ccdf4f0SGreg Roach     *
2316ccdf4f0SGreg Roach     * @return $this
2326ccdf4f0SGreg Roach     */
2336ccdf4f0SGreg Roach    public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree
2346ccdf4f0SGreg Roach    {
2356ccdf4f0SGreg Roach        if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
2366ccdf4f0SGreg Roach            // Update the database
2376ccdf4f0SGreg Roach            DB::table('user_gedcom_setting')->updateOrInsert([
2386ccdf4f0SGreg Roach                'gedcom_id'    => $this->id(),
2396ccdf4f0SGreg Roach                'user_id'      => $user->id(),
2406ccdf4f0SGreg Roach                'setting_name' => $setting_name,
2416ccdf4f0SGreg Roach            ], [
2426ccdf4f0SGreg Roach                'setting_value' => $setting_value,
2436ccdf4f0SGreg Roach            ]);
2446ccdf4f0SGreg Roach
2456ccdf4f0SGreg Roach            // Update the cache
2466ccdf4f0SGreg Roach            $this->user_preferences[$user->id()][$setting_name] = $setting_value;
2476ccdf4f0SGreg Roach            // Audit log of changes
2486ccdf4f0SGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this);
2496ccdf4f0SGreg Roach        }
2506ccdf4f0SGreg Roach
2516ccdf4f0SGreg Roach        return $this;
2526ccdf4f0SGreg Roach    }
2536ccdf4f0SGreg Roach
2546ccdf4f0SGreg Roach    /**
2556ccdf4f0SGreg Roach     * Get the tree’s user-configuration settings.
2566ccdf4f0SGreg Roach     *
2576ccdf4f0SGreg Roach     * @param UserInterface $user
2586ccdf4f0SGreg Roach     * @param string        $setting_name
2596ccdf4f0SGreg Roach     * @param string        $default
2606ccdf4f0SGreg Roach     *
2616ccdf4f0SGreg Roach     * @return string
2626ccdf4f0SGreg Roach     */
2636ccdf4f0SGreg Roach    public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string
2646ccdf4f0SGreg Roach    {
2656ccdf4f0SGreg Roach        // There are lots of settings, and we need to fetch lots of them on every page
2666ccdf4f0SGreg Roach        // so it is quicker to fetch them all in one go.
2676ccdf4f0SGreg Roach        if (!array_key_exists($user->id(), $this->user_preferences)) {
2686ccdf4f0SGreg Roach            $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting')
2696ccdf4f0SGreg Roach                ->where('user_id', '=', $user->id())
2706ccdf4f0SGreg Roach                ->where('gedcom_id', '=', $this->id)
2716ccdf4f0SGreg Roach                ->pluck('setting_value', 'setting_name')
2726ccdf4f0SGreg Roach                ->all();
2736ccdf4f0SGreg Roach        }
2746ccdf4f0SGreg Roach
2756ccdf4f0SGreg Roach        return $this->user_preferences[$user->id()][$setting_name] ?? $default;
2766ccdf4f0SGreg Roach    }
2776ccdf4f0SGreg Roach
2786ccdf4f0SGreg Roach    /**
2796ccdf4f0SGreg Roach     * The ID of this tree
2806ccdf4f0SGreg Roach     *
2816ccdf4f0SGreg Roach     * @return int
2826ccdf4f0SGreg Roach     */
2836ccdf4f0SGreg Roach    public function id(): int
2846ccdf4f0SGreg Roach    {
2856ccdf4f0SGreg Roach        return $this->id;
2866ccdf4f0SGreg Roach    }
2876ccdf4f0SGreg Roach
2886ccdf4f0SGreg Roach    /**
2896ccdf4f0SGreg Roach     * Can a user accept changes for this tree?
2906ccdf4f0SGreg Roach     *
2916ccdf4f0SGreg Roach     * @param UserInterface $user
2926ccdf4f0SGreg Roach     *
2936ccdf4f0SGreg Roach     * @return bool
2946ccdf4f0SGreg Roach     */
2956ccdf4f0SGreg Roach    public function canAcceptChanges(UserInterface $user): bool
2966ccdf4f0SGreg Roach    {
2976ccdf4f0SGreg Roach        return Auth::isModerator($this, $user);
2986ccdf4f0SGreg Roach    }
2996ccdf4f0SGreg Roach
3006ccdf4f0SGreg Roach    /**
301b78374c5SGreg Roach     * Are there any pending edits for this tree, than need reviewing by a moderator.
302b78374c5SGreg Roach     *
303b78374c5SGreg Roach     * @return bool
304b78374c5SGreg Roach     */
305771ae10aSGreg Roach    public function hasPendingEdit(): bool
306c1010edaSGreg Roach    {
30715a3f100SGreg Roach        return DB::table('change')
30815a3f100SGreg Roach            ->where('gedcom_id', '=', $this->id)
30915a3f100SGreg Roach            ->where('status', '=', 'pending')
31015a3f100SGreg Roach            ->exists();
311b78374c5SGreg Roach    }
312b78374c5SGreg Roach
313b78374c5SGreg Roach    /**
3146ccdf4f0SGreg Roach     * Delete everything relating to a tree
3156ccdf4f0SGreg Roach     *
3166ccdf4f0SGreg Roach     * @return void
3176ccdf4f0SGreg Roach     */
3186ccdf4f0SGreg Roach    public function delete(): void
3196ccdf4f0SGreg Roach    {
3206ccdf4f0SGreg Roach        // If this is the default tree, then unset it
3216ccdf4f0SGreg Roach        if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) {
3226ccdf4f0SGreg Roach            Site::setPreference('DEFAULT_GEDCOM', '');
3236ccdf4f0SGreg Roach        }
3246ccdf4f0SGreg Roach
3256ccdf4f0SGreg Roach        $this->deleteGenealogyData(false);
3266ccdf4f0SGreg Roach
3276ccdf4f0SGreg Roach        DB::table('block_setting')
3286ccdf4f0SGreg Roach            ->join('block', 'block.block_id', '=', 'block_setting.block_id')
3296ccdf4f0SGreg Roach            ->where('gedcom_id', '=', $this->id)
3306ccdf4f0SGreg Roach            ->delete();
3316ccdf4f0SGreg Roach        DB::table('block')->where('gedcom_id', '=', $this->id)->delete();
3326ccdf4f0SGreg Roach        DB::table('user_gedcom_setting')->where('gedcom_id', '=', $this->id)->delete();
3336ccdf4f0SGreg Roach        DB::table('gedcom_setting')->where('gedcom_id', '=', $this->id)->delete();
3346ccdf4f0SGreg Roach        DB::table('module_privacy')->where('gedcom_id', '=', $this->id)->delete();
3356ccdf4f0SGreg Roach        DB::table('hit_counter')->where('gedcom_id', '=', $this->id)->delete();
3366ccdf4f0SGreg Roach        DB::table('default_resn')->where('gedcom_id', '=', $this->id)->delete();
3376ccdf4f0SGreg Roach        DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete();
3386ccdf4f0SGreg Roach        DB::table('log')->where('gedcom_id', '=', $this->id)->delete();
3396ccdf4f0SGreg Roach        DB::table('gedcom')->where('gedcom_id', '=', $this->id)->delete();
3406ccdf4f0SGreg Roach    }
3416ccdf4f0SGreg Roach
3426ccdf4f0SGreg Roach    /**
343a25f0a04SGreg Roach     * Delete all the genealogy data from a tree - in preparation for importing
344a25f0a04SGreg Roach     * new data. Optionally retain the media data, for when the user has been
345a25f0a04SGreg Roach     * editing their data offline using an application which deletes (or does not
346a25f0a04SGreg Roach     * support) media data.
347a25f0a04SGreg Roach     *
348a25f0a04SGreg Roach     * @param bool $keep_media
349b7e60af1SGreg Roach     *
350b7e60af1SGreg Roach     * @return void
351a25f0a04SGreg Roach     */
352e364afe4SGreg Roach    public function deleteGenealogyData(bool $keep_media): void
353c1010edaSGreg Roach    {
3541ad2dde6SGreg Roach        DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete();
3551ad2dde6SGreg Roach        DB::table('individuals')->where('i_file', '=', $this->id)->delete();
3561ad2dde6SGreg Roach        DB::table('families')->where('f_file', '=', $this->id)->delete();
3571ad2dde6SGreg Roach        DB::table('sources')->where('s_file', '=', $this->id)->delete();
3581ad2dde6SGreg Roach        DB::table('other')->where('o_file', '=', $this->id)->delete();
3591ad2dde6SGreg Roach        DB::table('places')->where('p_file', '=', $this->id)->delete();
3601ad2dde6SGreg Roach        DB::table('placelinks')->where('pl_file', '=', $this->id)->delete();
3611ad2dde6SGreg Roach        DB::table('name')->where('n_file', '=', $this->id)->delete();
3621ad2dde6SGreg Roach        DB::table('dates')->where('d_file', '=', $this->id)->delete();
3631ad2dde6SGreg Roach        DB::table('change')->where('gedcom_id', '=', $this->id)->delete();
364a25f0a04SGreg Roach
365a25f0a04SGreg Roach        if ($keep_media) {
3661ad2dde6SGreg Roach            DB::table('link')->where('l_file', '=', $this->id)
3671ad2dde6SGreg Roach                ->where('l_type', '<>', 'OBJE')
3681ad2dde6SGreg Roach                ->delete();
369a25f0a04SGreg Roach        } else {
3701ad2dde6SGreg Roach            DB::table('link')->where('l_file', '=', $this->id)->delete();
3711ad2dde6SGreg Roach            DB::table('media_file')->where('m_file', '=', $this->id)->delete();
3721ad2dde6SGreg Roach            DB::table('media')->where('m_file', '=', $this->id)->delete();
373a25f0a04SGreg Roach        }
374a25f0a04SGreg Roach    }
375a25f0a04SGreg Roach
376a25f0a04SGreg Roach    /**
377a25f0a04SGreg Roach     * Export the tree to a GEDCOM file
378a25f0a04SGreg Roach     *
3795792757eSGreg Roach     * @param resource $stream
380b7e60af1SGreg Roach     *
381b7e60af1SGreg Roach     * @return void
38269c05a6eSGreg Roach     *
38369c05a6eSGreg Roach     * @deprecated since 2.0.5.  Will be removed in 2.1.0
384a25f0a04SGreg Roach     */
385425af8b9SGreg Roach    public function exportGedcom($stream): void
386c1010edaSGreg Roach    {
38769c05a6eSGreg Roach        $gedcom_export_service = new GedcomExportService();
38894026f20SGreg Roach
38969c05a6eSGreg Roach        $gedcom_export_service->export($this, $stream);
390a25f0a04SGreg Roach    }
391a25f0a04SGreg Roach
392a25f0a04SGreg Roach    /**
393a25f0a04SGreg Roach     * Import data from a gedcom file into this tree.
394a25f0a04SGreg Roach     *
3956ccdf4f0SGreg Roach     * @param StreamInterface $stream   The GEDCOM file.
396a25f0a04SGreg Roach     * @param string          $filename The preferred filename, for export/download.
397a25f0a04SGreg Roach     *
398b7e60af1SGreg Roach     * @return void
399a25f0a04SGreg Roach     */
4006ccdf4f0SGreg Roach    public function importGedcomFile(StreamInterface $stream, string $filename): void
401c1010edaSGreg Roach    {
402a25f0a04SGreg Roach        // Read the file in blocks of roughly 64K. Ensure that each block
403a25f0a04SGreg Roach        // contains complete gedcom records. This will ensure we don’t split
404a25f0a04SGreg Roach        // multi-byte characters, as well as simplifying the code to import
405a25f0a04SGreg Roach        // each block.
406a25f0a04SGreg Roach
407a25f0a04SGreg Roach        $file_data = '';
408a25f0a04SGreg Roach
409b7e60af1SGreg Roach        $this->deleteGenealogyData((bool) $this->getPreference('keep_media'));
410a25f0a04SGreg Roach        $this->setPreference('gedcom_filename', $filename);
411a25f0a04SGreg Roach        $this->setPreference('imported', '0');
412a25f0a04SGreg Roach
4136ccdf4f0SGreg Roach        while (!$stream->eof()) {
4146ccdf4f0SGreg Roach            $file_data .= $stream->read(65536);
415a25f0a04SGreg Roach            // There is no strrpos() function that searches for substrings :-(
416a25f0a04SGreg Roach            for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) {
417a25f0a04SGreg Roach                if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) {
418a25f0a04SGreg Roach                    // We’ve found the last record boundary in this chunk of data
419a25f0a04SGreg Roach                    break;
420a25f0a04SGreg Roach                }
421a25f0a04SGreg Roach            }
422a25f0a04SGreg Roach            if ($pos) {
4231ad2dde6SGreg Roach                DB::table('gedcom_chunk')->insert([
4241ad2dde6SGreg Roach                    'gedcom_id'  => $this->id,
4251ad2dde6SGreg Roach                    'chunk_data' => substr($file_data, 0, $pos),
426c1010edaSGreg Roach                ]);
4271ad2dde6SGreg Roach
428a25f0a04SGreg Roach                $file_data = substr($file_data, $pos);
429a25f0a04SGreg Roach            }
430a25f0a04SGreg Roach        }
4311ad2dde6SGreg Roach        DB::table('gedcom_chunk')->insert([
4321ad2dde6SGreg Roach            'gedcom_id'  => $this->id,
4331ad2dde6SGreg Roach            'chunk_data' => $file_data,
434c1010edaSGreg Roach        ]);
435a25f0a04SGreg Roach
4366ccdf4f0SGreg Roach        $stream->close();
4376ccdf4f0SGreg Roach    }
4386ccdf4f0SGreg Roach
4396ccdf4f0SGreg Roach    /**
4406ccdf4f0SGreg Roach     * Create a new record from GEDCOM data.
4416ccdf4f0SGreg Roach     *
4426ccdf4f0SGreg Roach     * @param string $gedcom
4436ccdf4f0SGreg Roach     *
4440d15532eSGreg Roach     * @return GedcomRecord|Individual|Family|Location|Note|Source|Repository|Media|Submitter|Submission
4456ccdf4f0SGreg Roach     * @throws InvalidArgumentException
4466ccdf4f0SGreg Roach     */
4476ccdf4f0SGreg Roach    public function createRecord(string $gedcom): GedcomRecord
4486ccdf4f0SGreg Roach    {
449b4a2f885SGreg Roach        if (!preg_match('/^0 @@ ([_A-Z]+)/', $gedcom, $match)) {
4506ccdf4f0SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@');
4516ccdf4f0SGreg Roach        }
4526ccdf4f0SGreg Roach
4536b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make($match[1]);
454dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
4556ccdf4f0SGreg Roach
4566ccdf4f0SGreg Roach        // Create a change record
45753432476SGreg Roach        $today = strtoupper(date('d M Y'));
45853432476SGreg Roach        $now   = date('H:i:s');
45953432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
4606ccdf4f0SGreg Roach
4616ccdf4f0SGreg Roach        // Create a pending change
4626ccdf4f0SGreg Roach        DB::table('change')->insert([
4636ccdf4f0SGreg Roach            'gedcom_id'  => $this->id,
4646ccdf4f0SGreg Roach            'xref'       => $xref,
4656ccdf4f0SGreg Roach            'old_gedcom' => '',
4666ccdf4f0SGreg Roach            'new_gedcom' => $gedcom,
4676ccdf4f0SGreg Roach            'user_id'    => Auth::id(),
4686ccdf4f0SGreg Roach        ]);
4696ccdf4f0SGreg Roach
4706ccdf4f0SGreg Roach        // Accept this pending change
471*1fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
4726b9cb339SGreg Roach            $record = Registry::gedcomRecordFactory()->new($xref, $gedcom, null, $this);
4736ccdf4f0SGreg Roach
47422e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
47522e73debSGreg Roach
47622e73debSGreg Roach            return $record;
4776ccdf4f0SGreg Roach        }
4786ccdf4f0SGreg Roach
4796b9cb339SGreg Roach        return Registry::gedcomRecordFactory()->new($xref, '', $gedcom, $this);
480a25f0a04SGreg Roach    }
481304f20d5SGreg Roach
482304f20d5SGreg Roach    /**
483b90d8accSGreg Roach     * Generate a new XREF, unique across all family trees
484b90d8accSGreg Roach     *
485b90d8accSGreg Roach     * @return string
486b4a2f885SGreg Roach     * @deprecated - use the factory directly.
487b90d8accSGreg Roach     */
488771ae10aSGreg Roach    public function getNewXref(): string
489c1010edaSGreg Roach    {
4906b9cb339SGreg Roach        return Registry::xrefFactory()->make(GedcomRecord::RECORD_TYPE);
491b90d8accSGreg Roach    }
492b90d8accSGreg Roach
493b90d8accSGreg Roach    /**
494afb591d7SGreg Roach     * Create a new family from GEDCOM data.
495afb591d7SGreg Roach     *
496afb591d7SGreg Roach     * @param string $gedcom
497afb591d7SGreg Roach     *
498afb591d7SGreg Roach     * @return Family
499afb591d7SGreg Roach     * @throws InvalidArgumentException
500afb591d7SGreg Roach     */
501afb591d7SGreg Roach    public function createFamily(string $gedcom): GedcomRecord
502afb591d7SGreg Roach    {
503dec352c1SGreg Roach        if (!str_starts_with($gedcom, '0 @@ FAM')) {
504afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM');
505afb591d7SGreg Roach        }
506afb591d7SGreg Roach
5076b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make(Family::RECORD_TYPE);
508dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
509afb591d7SGreg Roach
510afb591d7SGreg Roach        // Create a change record
51153432476SGreg Roach        $today = strtoupper(date('d M Y'));
51253432476SGreg Roach        $now   = date('H:i:s');
51353432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
514afb591d7SGreg Roach
515afb591d7SGreg Roach        // Create a pending change
516963fbaeeSGreg Roach        DB::table('change')->insert([
517963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
518963fbaeeSGreg Roach            'xref'       => $xref,
519963fbaeeSGreg Roach            'old_gedcom' => '',
520963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
521963fbaeeSGreg Roach            'user_id'    => Auth::id(),
522afb591d7SGreg Roach        ]);
523304f20d5SGreg Roach
524304f20d5SGreg Roach        // Accept this pending change
525*1fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
5266b9cb339SGreg Roach            $record = Registry::familyFactory()->new($xref, $gedcom, null, $this);
527afb591d7SGreg Roach
52822e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
52922e73debSGreg Roach
53022e73debSGreg Roach            return $record;
531304f20d5SGreg Roach        }
532afb591d7SGreg Roach
5336b9cb339SGreg Roach        return Registry::familyFactory()->new($xref, '', $gedcom, $this);
534afb591d7SGreg Roach    }
535afb591d7SGreg Roach
536afb591d7SGreg Roach    /**
537afb591d7SGreg Roach     * Create a new individual from GEDCOM data.
538afb591d7SGreg Roach     *
539afb591d7SGreg Roach     * @param string $gedcom
540afb591d7SGreg Roach     *
541afb591d7SGreg Roach     * @return Individual
542afb591d7SGreg Roach     * @throws InvalidArgumentException
543afb591d7SGreg Roach     */
544afb591d7SGreg Roach    public function createIndividual(string $gedcom): GedcomRecord
545afb591d7SGreg Roach    {
546dec352c1SGreg Roach        if (!str_starts_with($gedcom, '0 @@ INDI')) {
547afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI');
548afb591d7SGreg Roach        }
549afb591d7SGreg Roach
5506b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make(Individual::RECORD_TYPE);
551dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
552afb591d7SGreg Roach
553afb591d7SGreg Roach        // Create a change record
55453432476SGreg Roach        $today = strtoupper(date('d M Y'));
55553432476SGreg Roach        $now   = date('H:i:s');
55653432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
557afb591d7SGreg Roach
558afb591d7SGreg Roach        // Create a pending change
559963fbaeeSGreg Roach        DB::table('change')->insert([
560963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
561963fbaeeSGreg Roach            'xref'       => $xref,
562963fbaeeSGreg Roach            'old_gedcom' => '',
563963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
564963fbaeeSGreg Roach            'user_id'    => Auth::id(),
565afb591d7SGreg Roach        ]);
566afb591d7SGreg Roach
567afb591d7SGreg Roach        // Accept this pending change
568*1fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
5696b9cb339SGreg Roach            $record = Registry::individualFactory()->new($xref, $gedcom, null, $this);
570afb591d7SGreg Roach
57122e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
57222e73debSGreg Roach
57322e73debSGreg Roach            return $record;
574afb591d7SGreg Roach        }
575afb591d7SGreg Roach
5766b9cb339SGreg Roach        return Registry::individualFactory()->new($xref, '', $gedcom, $this);
577304f20d5SGreg Roach    }
5788586983fSGreg Roach
5798586983fSGreg Roach    /**
58020b58d20SGreg Roach     * Create a new media object from GEDCOM data.
58120b58d20SGreg Roach     *
58220b58d20SGreg Roach     * @param string $gedcom
58320b58d20SGreg Roach     *
58420b58d20SGreg Roach     * @return Media
58520b58d20SGreg Roach     * @throws InvalidArgumentException
58620b58d20SGreg Roach     */
58720b58d20SGreg Roach    public function createMediaObject(string $gedcom): Media
58820b58d20SGreg Roach    {
589dec352c1SGreg Roach        if (!str_starts_with($gedcom, '0 @@ OBJE')) {
59020b58d20SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE');
59120b58d20SGreg Roach        }
59220b58d20SGreg Roach
5936b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make(Media::RECORD_TYPE);
594dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
59520b58d20SGreg Roach
59620b58d20SGreg Roach        // Create a change record
59753432476SGreg Roach        $today = strtoupper(date('d M Y'));
59853432476SGreg Roach        $now   = date('H:i:s');
59953432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
60020b58d20SGreg Roach
60120b58d20SGreg Roach        // Create a pending change
602963fbaeeSGreg Roach        DB::table('change')->insert([
603963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
604963fbaeeSGreg Roach            'xref'       => $xref,
605963fbaeeSGreg Roach            'old_gedcom' => '',
606963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
607963fbaeeSGreg Roach            'user_id'    => Auth::id(),
60820b58d20SGreg Roach        ]);
60920b58d20SGreg Roach
61020b58d20SGreg Roach        // Accept this pending change
611*1fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
6126b9cb339SGreg Roach            $record = Registry::mediaFactory()->new($xref, $gedcom, null, $this);
61320b58d20SGreg Roach
61422e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
61522e73debSGreg Roach
61622e73debSGreg Roach            return $record;
61720b58d20SGreg Roach        }
61820b58d20SGreg Roach
6196b9cb339SGreg Roach        return Registry::mediaFactory()->new($xref, '', $gedcom, $this);
62020b58d20SGreg Roach    }
62120b58d20SGreg Roach
62220b58d20SGreg Roach    /**
6238586983fSGreg Roach     * What is the most significant individual in this tree.
6248586983fSGreg Roach     *
625e5a6b4d4SGreg Roach     * @param UserInterface $user
6263370567dSGreg Roach     * @param string        $xref
6278586983fSGreg Roach     *
6288586983fSGreg Roach     * @return Individual
6298586983fSGreg Roach     */
6303370567dSGreg Roach    public function significantIndividual(UserInterface $user, $xref = ''): Individual
631c1010edaSGreg Roach    {
6323370567dSGreg Roach        if ($xref === '') {
6338f9b0fb2SGreg Roach            $individual = null;
6343370567dSGreg Roach        } else {
6356b9cb339SGreg Roach            $individual = Registry::individualFactory()->make($xref, $this);
6363370567dSGreg Roach
6373370567dSGreg Roach            if ($individual === null) {
6386b9cb339SGreg Roach                $family = Registry::familyFactory()->make($xref, $this);
6393370567dSGreg Roach
6403370567dSGreg Roach                if ($family instanceof Family) {
6413370567dSGreg Roach                    $individual = $family->spouses()->first() ?? $family->children()->first();
6423370567dSGreg Roach                }
6433370567dSGreg Roach            }
6443370567dSGreg Roach        }
6458586983fSGreg Roach
646*1fe542e9SGreg Roach        if ($individual === null && $this->getUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF) !== '') {
647*1fe542e9SGreg Roach            $individual = Registry::individualFactory()->make($this->getUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF), $this);
6488586983fSGreg Roach        }
6498f9b0fb2SGreg Roach
650*1fe542e9SGreg Roach        if ($individual === null && $this->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF) !== '') {
651*1fe542e9SGreg Roach            $individual = Registry::individualFactory()->make($this->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF), $this);
6528586983fSGreg Roach        }
6538f9b0fb2SGreg Roach
654bec87e94SGreg Roach        if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') {
6556b9cb339SGreg Roach            $individual = Registry::individualFactory()->make($this->getPreference('PEDIGREE_ROOT_ID'), $this);
6568586983fSGreg Roach        }
6578f9b0fb2SGreg Roach        if ($individual === null) {
6588f9b0fb2SGreg Roach            $xref = (string) DB::table('individuals')
6598f9b0fb2SGreg Roach                ->where('i_file', '=', $this->id())
6608f9b0fb2SGreg Roach                ->min('i_id');
661769d7d6eSGreg Roach
6626b9cb339SGreg Roach            $individual = Registry::individualFactory()->make($xref, $this);
6635fe1add5SGreg Roach        }
6648f9b0fb2SGreg Roach        if ($individual === null) {
6655fe1add5SGreg Roach            // always return a record
6666b9cb339SGreg Roach            $individual = Registry::individualFactory()->new('I', '0 @I@ INDI', null, $this);
6675fe1add5SGreg Roach        }
6685fe1add5SGreg Roach
6695fe1add5SGreg Roach        return $individual;
6705fe1add5SGreg Roach    }
6711df7ae39SGreg Roach
67285a166d8SGreg Roach    /**
67385a166d8SGreg Roach     * Where do we store our media files.
67485a166d8SGreg Roach     *
675a04bb9a2SGreg Roach     * @param FilesystemInterface $data_filesystem
676a04bb9a2SGreg Roach     *
67785a166d8SGreg Roach     * @return FilesystemInterface
67885a166d8SGreg Roach     */
679a04bb9a2SGreg Roach    public function mediaFilesystem(FilesystemInterface $data_filesystem): FilesystemInterface
6801df7ae39SGreg Roach    {
681456d0d35SGreg Roach        $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/');
682a04bb9a2SGreg Roach        $adapter   = new ChrootAdapter($data_filesystem, $media_dir);
683456d0d35SGreg Roach
684456d0d35SGreg Roach        return new Filesystem($adapter);
6851df7ae39SGreg Roach    }
686a25f0a04SGreg Roach}
687