xref: /webtrees/app/Tree.php (revision 69c05a6e92d49a04585e420425781681b3eaff1c)
1a25f0a04SGreg Roach<?php
23976b470SGreg Roach
3a25f0a04SGreg Roach/**
4a25f0a04SGreg Roach * webtrees: online genealogy
5a091ac74SGreg Roach * Copyright (C) 2020 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;
26*69c05a6eSGreg Roachuse Fisharebest\Webtrees\Services\GedcomExportService;
2722e73debSGreg Roachuse Fisharebest\Webtrees\Services\PendingChangesService;
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
381e653452SGreg Roachuse function app;
3953432476SGreg Roachuse function date;
4053432476SGreg Roachuse function strtoupper;
411e653452SGreg Roach
42a25f0a04SGreg Roach/**
4376692c8bSGreg Roach * Provide an interface to the wt_gedcom table.
44a25f0a04SGreg Roach */
45c1010edaSGreg Roachclass Tree
46c1010edaSGreg Roach{
47061b43d7SGreg Roach    private const RESN_PRIVACY = [
48061b43d7SGreg Roach        'none'         => Auth::PRIV_PRIVATE,
49061b43d7SGreg Roach        'privacy'      => Auth::PRIV_USER,
50061b43d7SGreg Roach        'confidential' => Auth::PRIV_NONE,
51061b43d7SGreg Roach        'hidden'       => Auth::PRIV_HIDE,
52061b43d7SGreg Roach    ];
533df1e584SGreg Roach
546ccdf4f0SGreg Roach    /** @var int The tree's ID number */
556ccdf4f0SGreg Roach    private $id;
563df1e584SGreg Roach
576ccdf4f0SGreg Roach    /** @var string The tree's name */
586ccdf4f0SGreg Roach    private $name;
593df1e584SGreg Roach
606ccdf4f0SGreg Roach    /** @var string The tree's title */
616ccdf4f0SGreg Roach    private $title;
623df1e584SGreg Roach
636ccdf4f0SGreg Roach    /** @var int[] Default access rules for facts in this tree */
646ccdf4f0SGreg Roach    private $fact_privacy;
653df1e584SGreg Roach
666ccdf4f0SGreg Roach    /** @var int[] Default access rules for individuals in this tree */
676ccdf4f0SGreg Roach    private $individual_privacy;
683df1e584SGreg Roach
696ccdf4f0SGreg Roach    /** @var integer[][] Default access rules for individual facts in this tree */
706ccdf4f0SGreg Roach    private $individual_fact_privacy;
713df1e584SGreg Roach
726ccdf4f0SGreg Roach    /** @var string[] Cached copy of the wt_gedcom_setting table. */
736ccdf4f0SGreg Roach    private $preferences = [];
743df1e584SGreg Roach
756ccdf4f0SGreg Roach    /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */
766ccdf4f0SGreg Roach    private $user_preferences = [];
77061b43d7SGreg Roach
78a25f0a04SGreg Roach    /**
793df1e584SGreg Roach     * Create a tree object.
80a25f0a04SGreg Roach     *
8172cf66d4SGreg Roach     * @param int    $id
82aa6f03bbSGreg Roach     * @param string $name
83cc13d6d8SGreg Roach     * @param string $title
84a25f0a04SGreg Roach     */
855afbc57aSGreg Roach    public function __construct(int $id, string $name, string $title)
86c1010edaSGreg Roach    {
8772cf66d4SGreg Roach        $this->id                      = $id;
88aa6f03bbSGreg Roach        $this->name                    = $name;
89cc13d6d8SGreg Roach        $this->title                   = $title;
9013abd6f3SGreg Roach        $this->fact_privacy            = [];
9113abd6f3SGreg Roach        $this->individual_privacy      = [];
9213abd6f3SGreg Roach        $this->individual_fact_privacy = [];
93518bbdc1SGreg Roach
94518bbdc1SGreg Roach        // Load the privacy settings for this tree
95061b43d7SGreg Roach        $rows = DB::table('default_resn')
96061b43d7SGreg Roach            ->where('gedcom_id', '=', $this->id)
97061b43d7SGreg Roach            ->get();
98518bbdc1SGreg Roach
99518bbdc1SGreg Roach        foreach ($rows as $row) {
100061b43d7SGreg Roach            // Convert GEDCOM privacy restriction to a webtrees access level.
101061b43d7SGreg Roach            $row->resn = self::RESN_PRIVACY[$row->resn];
102061b43d7SGreg Roach
103518bbdc1SGreg Roach            if ($row->xref !== null) {
104518bbdc1SGreg Roach                if ($row->tag_type !== null) {
105b262b3d3SGreg Roach                    $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn;
106518bbdc1SGreg Roach                } else {
107b262b3d3SGreg Roach                    $this->individual_privacy[$row->xref] = $row->resn;
108518bbdc1SGreg Roach                }
109518bbdc1SGreg Roach            } else {
110b262b3d3SGreg Roach                $this->fact_privacy[$row->tag_type] = $row->resn;
111518bbdc1SGreg Roach            }
112518bbdc1SGreg Roach        }
113a25f0a04SGreg Roach    }
114a25f0a04SGreg Roach
115a25f0a04SGreg Roach    /**
1165afbc57aSGreg Roach     * A closure which will create a record from a database row.
1175afbc57aSGreg Roach     *
1185afbc57aSGreg Roach     * @return Closure
1195afbc57aSGreg Roach     */
1205afbc57aSGreg Roach    public static function rowMapper(): Closure
1215afbc57aSGreg Roach    {
1225afbc57aSGreg Roach        return static function (stdClass $row): Tree {
1235afbc57aSGreg Roach            return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title);
1245afbc57aSGreg Roach        };
1255afbc57aSGreg Roach    }
1265afbc57aSGreg Roach
1275afbc57aSGreg Roach    /**
1286ccdf4f0SGreg Roach     * Set the tree’s configuration settings.
1296ccdf4f0SGreg Roach     *
1306ccdf4f0SGreg Roach     * @param string $setting_name
1316ccdf4f0SGreg Roach     * @param string $setting_value
1326ccdf4f0SGreg Roach     *
1336ccdf4f0SGreg Roach     * @return $this
1346ccdf4f0SGreg Roach     */
1356ccdf4f0SGreg Roach    public function setPreference(string $setting_name, string $setting_value): Tree
1366ccdf4f0SGreg Roach    {
1376ccdf4f0SGreg Roach        if ($setting_value !== $this->getPreference($setting_name)) {
1386ccdf4f0SGreg Roach            DB::table('gedcom_setting')->updateOrInsert([
1396ccdf4f0SGreg Roach                'gedcom_id'    => $this->id,
1406ccdf4f0SGreg Roach                'setting_name' => $setting_name,
1416ccdf4f0SGreg Roach            ], [
1426ccdf4f0SGreg Roach                'setting_value' => $setting_value,
1436ccdf4f0SGreg Roach            ]);
1446ccdf4f0SGreg Roach
1456ccdf4f0SGreg Roach            $this->preferences[$setting_name] = $setting_value;
1466ccdf4f0SGreg Roach
1476ccdf4f0SGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this);
1486ccdf4f0SGreg Roach        }
1496ccdf4f0SGreg Roach
1506ccdf4f0SGreg Roach        return $this;
1516ccdf4f0SGreg Roach    }
1526ccdf4f0SGreg Roach
1536ccdf4f0SGreg Roach    /**
1546ccdf4f0SGreg Roach     * Get the tree’s configuration settings.
1556ccdf4f0SGreg Roach     *
1566ccdf4f0SGreg Roach     * @param string $setting_name
1576ccdf4f0SGreg Roach     * @param string $default
1586ccdf4f0SGreg Roach     *
1596ccdf4f0SGreg Roach     * @return string
1606ccdf4f0SGreg Roach     */
1616ccdf4f0SGreg Roach    public function getPreference(string $setting_name, string $default = ''): string
1626ccdf4f0SGreg Roach    {
16354c1ab5eSGreg Roach        if ($this->preferences === []) {
1646ccdf4f0SGreg Roach            $this->preferences = DB::table('gedcom_setting')
1656ccdf4f0SGreg Roach                ->where('gedcom_id', '=', $this->id)
1666ccdf4f0SGreg Roach                ->pluck('setting_value', 'setting_name')
1676ccdf4f0SGreg Roach                ->all();
1686ccdf4f0SGreg Roach        }
1696ccdf4f0SGreg Roach
1706ccdf4f0SGreg Roach        return $this->preferences[$setting_name] ?? $default;
1716ccdf4f0SGreg Roach    }
1726ccdf4f0SGreg Roach
1736ccdf4f0SGreg Roach    /**
1746ccdf4f0SGreg Roach     * The name of this tree
1756ccdf4f0SGreg Roach     *
1766ccdf4f0SGreg Roach     * @return string
1776ccdf4f0SGreg Roach     */
1786ccdf4f0SGreg Roach    public function name(): string
1796ccdf4f0SGreg Roach    {
1806ccdf4f0SGreg Roach        return $this->name;
1816ccdf4f0SGreg Roach    }
1826ccdf4f0SGreg Roach
1836ccdf4f0SGreg Roach    /**
1846ccdf4f0SGreg Roach     * The title of this tree
1856ccdf4f0SGreg Roach     *
1866ccdf4f0SGreg Roach     * @return string
1876ccdf4f0SGreg Roach     */
1886ccdf4f0SGreg Roach    public function title(): string
1896ccdf4f0SGreg Roach    {
1906ccdf4f0SGreg Roach        return $this->title;
1916ccdf4f0SGreg Roach    }
1926ccdf4f0SGreg Roach
1936ccdf4f0SGreg Roach    /**
1946ccdf4f0SGreg Roach     * The fact-level privacy for this tree.
1956ccdf4f0SGreg Roach     *
1966ccdf4f0SGreg Roach     * @return int[]
1976ccdf4f0SGreg Roach     */
1986ccdf4f0SGreg Roach    public function getFactPrivacy(): array
1996ccdf4f0SGreg Roach    {
2006ccdf4f0SGreg Roach        return $this->fact_privacy;
2016ccdf4f0SGreg Roach    }
2026ccdf4f0SGreg Roach
2036ccdf4f0SGreg Roach    /**
2046ccdf4f0SGreg Roach     * The individual-level privacy for this tree.
2056ccdf4f0SGreg Roach     *
2066ccdf4f0SGreg Roach     * @return int[]
2076ccdf4f0SGreg Roach     */
2086ccdf4f0SGreg Roach    public function getIndividualPrivacy(): array
2096ccdf4f0SGreg Roach    {
2106ccdf4f0SGreg Roach        return $this->individual_privacy;
2116ccdf4f0SGreg Roach    }
2126ccdf4f0SGreg Roach
2136ccdf4f0SGreg Roach    /**
2146ccdf4f0SGreg Roach     * The individual-fact-level privacy for this tree.
2156ccdf4f0SGreg Roach     *
2166ccdf4f0SGreg Roach     * @return int[][]
2176ccdf4f0SGreg Roach     */
2186ccdf4f0SGreg Roach    public function getIndividualFactPrivacy(): array
2196ccdf4f0SGreg Roach    {
2206ccdf4f0SGreg Roach        return $this->individual_fact_privacy;
2216ccdf4f0SGreg Roach    }
2226ccdf4f0SGreg Roach
2236ccdf4f0SGreg Roach    /**
2246ccdf4f0SGreg Roach     * Set the tree’s user-configuration settings.
2256ccdf4f0SGreg Roach     *
2266ccdf4f0SGreg Roach     * @param UserInterface $user
2276ccdf4f0SGreg Roach     * @param string        $setting_name
2286ccdf4f0SGreg Roach     * @param string        $setting_value
2296ccdf4f0SGreg Roach     *
2306ccdf4f0SGreg Roach     * @return $this
2316ccdf4f0SGreg Roach     */
2326ccdf4f0SGreg Roach    public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree
2336ccdf4f0SGreg Roach    {
2346ccdf4f0SGreg Roach        if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
2356ccdf4f0SGreg Roach            // Update the database
2366ccdf4f0SGreg Roach            DB::table('user_gedcom_setting')->updateOrInsert([
2376ccdf4f0SGreg Roach                'gedcom_id'    => $this->id(),
2386ccdf4f0SGreg Roach                'user_id'      => $user->id(),
2396ccdf4f0SGreg Roach                'setting_name' => $setting_name,
2406ccdf4f0SGreg Roach            ], [
2416ccdf4f0SGreg Roach                'setting_value' => $setting_value,
2426ccdf4f0SGreg Roach            ]);
2436ccdf4f0SGreg Roach
2446ccdf4f0SGreg Roach            // Update the cache
2456ccdf4f0SGreg Roach            $this->user_preferences[$user->id()][$setting_name] = $setting_value;
2466ccdf4f0SGreg Roach            // Audit log of changes
2476ccdf4f0SGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this);
2486ccdf4f0SGreg Roach        }
2496ccdf4f0SGreg Roach
2506ccdf4f0SGreg Roach        return $this;
2516ccdf4f0SGreg Roach    }
2526ccdf4f0SGreg Roach
2536ccdf4f0SGreg Roach    /**
2546ccdf4f0SGreg Roach     * Get the tree’s user-configuration settings.
2556ccdf4f0SGreg Roach     *
2566ccdf4f0SGreg Roach     * @param UserInterface $user
2576ccdf4f0SGreg Roach     * @param string        $setting_name
2586ccdf4f0SGreg Roach     * @param string        $default
2596ccdf4f0SGreg Roach     *
2606ccdf4f0SGreg Roach     * @return string
2616ccdf4f0SGreg Roach     */
2626ccdf4f0SGreg Roach    public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string
2636ccdf4f0SGreg Roach    {
2646ccdf4f0SGreg Roach        // There are lots of settings, and we need to fetch lots of them on every page
2656ccdf4f0SGreg Roach        // so it is quicker to fetch them all in one go.
2666ccdf4f0SGreg Roach        if (!array_key_exists($user->id(), $this->user_preferences)) {
2676ccdf4f0SGreg Roach            $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting')
2686ccdf4f0SGreg Roach                ->where('user_id', '=', $user->id())
2696ccdf4f0SGreg Roach                ->where('gedcom_id', '=', $this->id)
2706ccdf4f0SGreg Roach                ->pluck('setting_value', 'setting_name')
2716ccdf4f0SGreg Roach                ->all();
2726ccdf4f0SGreg Roach        }
2736ccdf4f0SGreg Roach
2746ccdf4f0SGreg Roach        return $this->user_preferences[$user->id()][$setting_name] ?? $default;
2756ccdf4f0SGreg Roach    }
2766ccdf4f0SGreg Roach
2776ccdf4f0SGreg Roach    /**
2786ccdf4f0SGreg Roach     * The ID of this tree
2796ccdf4f0SGreg Roach     *
2806ccdf4f0SGreg Roach     * @return int
2816ccdf4f0SGreg Roach     */
2826ccdf4f0SGreg Roach    public function id(): int
2836ccdf4f0SGreg Roach    {
2846ccdf4f0SGreg Roach        return $this->id;
2856ccdf4f0SGreg Roach    }
2866ccdf4f0SGreg Roach
2876ccdf4f0SGreg Roach    /**
2886ccdf4f0SGreg Roach     * Can a user accept changes for this tree?
2896ccdf4f0SGreg Roach     *
2906ccdf4f0SGreg Roach     * @param UserInterface $user
2916ccdf4f0SGreg Roach     *
2926ccdf4f0SGreg Roach     * @return bool
2936ccdf4f0SGreg Roach     */
2946ccdf4f0SGreg Roach    public function canAcceptChanges(UserInterface $user): bool
2956ccdf4f0SGreg Roach    {
2966ccdf4f0SGreg Roach        return Auth::isModerator($this, $user);
2976ccdf4f0SGreg Roach    }
2986ccdf4f0SGreg Roach
2996ccdf4f0SGreg Roach    /**
300b78374c5SGreg Roach     * Are there any pending edits for this tree, than need reviewing by a moderator.
301b78374c5SGreg Roach     *
302b78374c5SGreg Roach     * @return bool
303b78374c5SGreg Roach     */
304771ae10aSGreg Roach    public function hasPendingEdit(): bool
305c1010edaSGreg Roach    {
30615a3f100SGreg Roach        return DB::table('change')
30715a3f100SGreg Roach            ->where('gedcom_id', '=', $this->id)
30815a3f100SGreg Roach            ->where('status', '=', 'pending')
30915a3f100SGreg Roach            ->exists();
310b78374c5SGreg Roach    }
311b78374c5SGreg Roach
312b78374c5SGreg Roach    /**
3136ccdf4f0SGreg Roach     * Delete everything relating to a tree
3146ccdf4f0SGreg Roach     *
3156ccdf4f0SGreg Roach     * @return void
3166ccdf4f0SGreg Roach     */
3176ccdf4f0SGreg Roach    public function delete(): void
3186ccdf4f0SGreg Roach    {
3196ccdf4f0SGreg Roach        // If this is the default tree, then unset it
3206ccdf4f0SGreg Roach        if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) {
3216ccdf4f0SGreg Roach            Site::setPreference('DEFAULT_GEDCOM', '');
3226ccdf4f0SGreg Roach        }
3236ccdf4f0SGreg Roach
3246ccdf4f0SGreg Roach        $this->deleteGenealogyData(false);
3256ccdf4f0SGreg Roach
3266ccdf4f0SGreg Roach        DB::table('block_setting')
3276ccdf4f0SGreg Roach            ->join('block', 'block.block_id', '=', 'block_setting.block_id')
3286ccdf4f0SGreg Roach            ->where('gedcom_id', '=', $this->id)
3296ccdf4f0SGreg Roach            ->delete();
3306ccdf4f0SGreg Roach        DB::table('block')->where('gedcom_id', '=', $this->id)->delete();
3316ccdf4f0SGreg Roach        DB::table('user_gedcom_setting')->where('gedcom_id', '=', $this->id)->delete();
3326ccdf4f0SGreg Roach        DB::table('gedcom_setting')->where('gedcom_id', '=', $this->id)->delete();
3336ccdf4f0SGreg Roach        DB::table('module_privacy')->where('gedcom_id', '=', $this->id)->delete();
3346ccdf4f0SGreg Roach        DB::table('hit_counter')->where('gedcom_id', '=', $this->id)->delete();
3356ccdf4f0SGreg Roach        DB::table('default_resn')->where('gedcom_id', '=', $this->id)->delete();
3366ccdf4f0SGreg Roach        DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete();
3376ccdf4f0SGreg Roach        DB::table('log')->where('gedcom_id', '=', $this->id)->delete();
3386ccdf4f0SGreg Roach        DB::table('gedcom')->where('gedcom_id', '=', $this->id)->delete();
3396ccdf4f0SGreg Roach    }
3406ccdf4f0SGreg Roach
3416ccdf4f0SGreg Roach    /**
342a25f0a04SGreg Roach     * Delete all the genealogy data from a tree - in preparation for importing
343a25f0a04SGreg Roach     * new data. Optionally retain the media data, for when the user has been
344a25f0a04SGreg Roach     * editing their data offline using an application which deletes (or does not
345a25f0a04SGreg Roach     * support) media data.
346a25f0a04SGreg Roach     *
347a25f0a04SGreg Roach     * @param bool $keep_media
348b7e60af1SGreg Roach     *
349b7e60af1SGreg Roach     * @return void
350a25f0a04SGreg Roach     */
351e364afe4SGreg Roach    public function deleteGenealogyData(bool $keep_media): void
352c1010edaSGreg Roach    {
3531ad2dde6SGreg Roach        DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete();
3541ad2dde6SGreg Roach        DB::table('individuals')->where('i_file', '=', $this->id)->delete();
3551ad2dde6SGreg Roach        DB::table('families')->where('f_file', '=', $this->id)->delete();
3561ad2dde6SGreg Roach        DB::table('sources')->where('s_file', '=', $this->id)->delete();
3571ad2dde6SGreg Roach        DB::table('other')->where('o_file', '=', $this->id)->delete();
3581ad2dde6SGreg Roach        DB::table('places')->where('p_file', '=', $this->id)->delete();
3591ad2dde6SGreg Roach        DB::table('placelinks')->where('pl_file', '=', $this->id)->delete();
3601ad2dde6SGreg Roach        DB::table('name')->where('n_file', '=', $this->id)->delete();
3611ad2dde6SGreg Roach        DB::table('dates')->where('d_file', '=', $this->id)->delete();
3621ad2dde6SGreg Roach        DB::table('change')->where('gedcom_id', '=', $this->id)->delete();
363a25f0a04SGreg Roach
364a25f0a04SGreg Roach        if ($keep_media) {
3651ad2dde6SGreg Roach            DB::table('link')->where('l_file', '=', $this->id)
3661ad2dde6SGreg Roach                ->where('l_type', '<>', 'OBJE')
3671ad2dde6SGreg Roach                ->delete();
368a25f0a04SGreg Roach        } else {
3691ad2dde6SGreg Roach            DB::table('link')->where('l_file', '=', $this->id)->delete();
3701ad2dde6SGreg Roach            DB::table('media_file')->where('m_file', '=', $this->id)->delete();
3711ad2dde6SGreg Roach            DB::table('media')->where('m_file', '=', $this->id)->delete();
372a25f0a04SGreg Roach        }
373a25f0a04SGreg Roach    }
374a25f0a04SGreg Roach
375a25f0a04SGreg Roach    /**
376a25f0a04SGreg Roach     * Export the tree to a GEDCOM file
377a25f0a04SGreg Roach     *
3785792757eSGreg Roach     * @param resource $stream
379b7e60af1SGreg Roach     *
380b7e60af1SGreg Roach     * @return void
381*69c05a6eSGreg Roach     *
382*69c05a6eSGreg Roach     * @deprecated since 2.0.5.  Will be removed in 2.1.0
383a25f0a04SGreg Roach     */
384425af8b9SGreg Roach    public function exportGedcom($stream): void
385c1010edaSGreg Roach    {
386*69c05a6eSGreg Roach        $gedcom_export_service = new GedcomExportService();
38794026f20SGreg Roach
388*69c05a6eSGreg Roach        $gedcom_export_service->export($this, $stream);
389a25f0a04SGreg Roach    }
390a25f0a04SGreg Roach
391a25f0a04SGreg Roach    /**
392a25f0a04SGreg Roach     * Import data from a gedcom file into this tree.
393a25f0a04SGreg Roach     *
3946ccdf4f0SGreg Roach     * @param StreamInterface $stream   The GEDCOM file.
395a25f0a04SGreg Roach     * @param string          $filename The preferred filename, for export/download.
396a25f0a04SGreg Roach     *
397b7e60af1SGreg Roach     * @return void
398a25f0a04SGreg Roach     */
3996ccdf4f0SGreg Roach    public function importGedcomFile(StreamInterface $stream, string $filename): void
400c1010edaSGreg Roach    {
401a25f0a04SGreg Roach        // Read the file in blocks of roughly 64K. Ensure that each block
402a25f0a04SGreg Roach        // contains complete gedcom records. This will ensure we don’t split
403a25f0a04SGreg Roach        // multi-byte characters, as well as simplifying the code to import
404a25f0a04SGreg Roach        // each block.
405a25f0a04SGreg Roach
406a25f0a04SGreg Roach        $file_data = '';
407a25f0a04SGreg Roach
408b7e60af1SGreg Roach        $this->deleteGenealogyData((bool) $this->getPreference('keep_media'));
409a25f0a04SGreg Roach        $this->setPreference('gedcom_filename', $filename);
410a25f0a04SGreg Roach        $this->setPreference('imported', '0');
411a25f0a04SGreg Roach
4126ccdf4f0SGreg Roach        while (!$stream->eof()) {
4136ccdf4f0SGreg Roach            $file_data .= $stream->read(65536);
414a25f0a04SGreg Roach            // There is no strrpos() function that searches for substrings :-(
415a25f0a04SGreg Roach            for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) {
416a25f0a04SGreg Roach                if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) {
417a25f0a04SGreg Roach                    // We’ve found the last record boundary in this chunk of data
418a25f0a04SGreg Roach                    break;
419a25f0a04SGreg Roach                }
420a25f0a04SGreg Roach            }
421a25f0a04SGreg Roach            if ($pos) {
4221ad2dde6SGreg Roach                DB::table('gedcom_chunk')->insert([
4231ad2dde6SGreg Roach                    'gedcom_id'  => $this->id,
4241ad2dde6SGreg Roach                    'chunk_data' => substr($file_data, 0, $pos),
425c1010edaSGreg Roach                ]);
4261ad2dde6SGreg Roach
427a25f0a04SGreg Roach                $file_data = substr($file_data, $pos);
428a25f0a04SGreg Roach            }
429a25f0a04SGreg Roach        }
4301ad2dde6SGreg Roach        DB::table('gedcom_chunk')->insert([
4311ad2dde6SGreg Roach            'gedcom_id'  => $this->id,
4321ad2dde6SGreg Roach            'chunk_data' => $file_data,
433c1010edaSGreg Roach        ]);
434a25f0a04SGreg Roach
4356ccdf4f0SGreg Roach        $stream->close();
4366ccdf4f0SGreg Roach    }
4376ccdf4f0SGreg Roach
4386ccdf4f0SGreg Roach    /**
4396ccdf4f0SGreg Roach     * Create a new record from GEDCOM data.
4406ccdf4f0SGreg Roach     *
4416ccdf4f0SGreg Roach     * @param string $gedcom
4426ccdf4f0SGreg Roach     *
4430d15532eSGreg Roach     * @return GedcomRecord|Individual|Family|Location|Note|Source|Repository|Media|Submitter|Submission
4446ccdf4f0SGreg Roach     * @throws InvalidArgumentException
4456ccdf4f0SGreg Roach     */
4466ccdf4f0SGreg Roach    public function createRecord(string $gedcom): GedcomRecord
4476ccdf4f0SGreg Roach    {
4486ccdf4f0SGreg Roach        if (!Str::startsWith($gedcom, '0 @@ ')) {
4496ccdf4f0SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@');
4506ccdf4f0SGreg Roach        }
4516ccdf4f0SGreg Roach
4526ccdf4f0SGreg Roach        $xref   = $this->getNewXref();
4536ccdf4f0SGreg Roach        $gedcom = '0 @' . $xref . '@ ' . Str::after($gedcom, '0 @@ ');
4546ccdf4f0SGreg Roach
4556ccdf4f0SGreg Roach        // Create a change record
45653432476SGreg Roach        $today = strtoupper(date('d M Y'));
45753432476SGreg Roach        $now   = date('H:i:s');
45853432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
4596ccdf4f0SGreg Roach
4606ccdf4f0SGreg Roach        // Create a pending change
4616ccdf4f0SGreg Roach        DB::table('change')->insert([
4626ccdf4f0SGreg Roach            'gedcom_id'  => $this->id,
4636ccdf4f0SGreg Roach            'xref'       => $xref,
4646ccdf4f0SGreg Roach            'old_gedcom' => '',
4656ccdf4f0SGreg Roach            'new_gedcom' => $gedcom,
4666ccdf4f0SGreg Roach            'user_id'    => Auth::id(),
4676ccdf4f0SGreg Roach        ]);
4686ccdf4f0SGreg Roach
4696ccdf4f0SGreg Roach        // Accept this pending change
4707c4add84SGreg Roach        if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS)) {
471a091ac74SGreg Roach            $record = Factory::gedcomRecord()->new($xref, $gedcom, null, $this);
4726ccdf4f0SGreg Roach
47322e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
47422e73debSGreg Roach
47522e73debSGreg Roach            return $record;
4766ccdf4f0SGreg Roach        }
4776ccdf4f0SGreg Roach
478a091ac74SGreg Roach        return Factory::gedcomRecord()->new($xref, '', $gedcom, $this);
479a25f0a04SGreg Roach    }
480304f20d5SGreg Roach
481304f20d5SGreg Roach    /**
482b90d8accSGreg Roach     * Generate a new XREF, unique across all family trees
483b90d8accSGreg Roach     *
484b90d8accSGreg Roach     * @return string
485b90d8accSGreg Roach     */
486771ae10aSGreg Roach    public function getNewXref(): string
487c1010edaSGreg Roach    {
488963fbaeeSGreg Roach        // Lock the row, so that only one new XREF may be generated at a time.
489963fbaeeSGreg Roach        DB::table('site_setting')
490963fbaeeSGreg Roach            ->where('setting_name', '=', 'next_xref')
491963fbaeeSGreg Roach            ->lockForUpdate()
492963fbaeeSGreg Roach            ->get();
493963fbaeeSGreg Roach
494a214e186SGreg Roach        $prefix = 'X';
495b90d8accSGreg Roach
496971d66c8SGreg Roach        $increment = 1.0;
497b90d8accSGreg Roach        do {
498963fbaeeSGreg Roach            $num = (int) Site::getPreference('next_xref') + (int) $increment;
499971d66c8SGreg Roach
500971d66c8SGreg Roach            // This exponential increment allows us to scan over large blocks of
501971d66c8SGreg Roach            // existing data in a reasonable time.
502971d66c8SGreg Roach            $increment *= 1.01;
503963fbaeeSGreg Roach
504963fbaeeSGreg Roach            $xref = $prefix . $num;
505963fbaeeSGreg Roach
506963fbaeeSGreg Roach            // Records may already exist with this sequence number.
507963fbaeeSGreg Roach            $already_used =
508963fbaeeSGreg Roach                DB::table('individuals')->where('i_id', '=', $xref)->exists() ||
509963fbaeeSGreg Roach                DB::table('families')->where('f_id', '=', $xref)->exists() ||
510963fbaeeSGreg Roach                DB::table('sources')->where('s_id', '=', $xref)->exists() ||
511963fbaeeSGreg Roach                DB::table('media')->where('m_id', '=', $xref)->exists() ||
512963fbaeeSGreg Roach                DB::table('other')->where('o_id', '=', $xref)->exists() ||
513963fbaeeSGreg Roach                DB::table('change')->where('xref', '=', $xref)->exists();
514963fbaeeSGreg Roach        } while ($already_used);
515963fbaeeSGreg Roach
516963fbaeeSGreg Roach        Site::setPreference('next_xref', (string) $num);
517b90d8accSGreg Roach
518a214e186SGreg Roach        return $xref;
519b90d8accSGreg Roach    }
520b90d8accSGreg Roach
521b90d8accSGreg Roach    /**
522afb591d7SGreg Roach     * Create a new family from GEDCOM data.
523afb591d7SGreg Roach     *
524afb591d7SGreg Roach     * @param string $gedcom
525afb591d7SGreg Roach     *
526afb591d7SGreg Roach     * @return Family
527afb591d7SGreg Roach     * @throws InvalidArgumentException
528afb591d7SGreg Roach     */
529afb591d7SGreg Roach    public function createFamily(string $gedcom): GedcomRecord
530afb591d7SGreg Roach    {
531bec87e94SGreg Roach        if (!Str::startsWith($gedcom, '0 @@ FAM')) {
532afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM');
533afb591d7SGreg Roach        }
534afb591d7SGreg Roach
535afb591d7SGreg Roach        $xref   = $this->getNewXref();
536bec87e94SGreg Roach        $gedcom = '0 @' . $xref . '@ FAM' . Str::after($gedcom, '0 @@ FAM');
537afb591d7SGreg Roach
538afb591d7SGreg Roach        // Create a change record
53953432476SGreg Roach        $today = strtoupper(date('d M Y'));
54053432476SGreg Roach        $now   = date('H:i:s');
54153432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
542afb591d7SGreg Roach
543afb591d7SGreg Roach        // Create a pending change
544963fbaeeSGreg Roach        DB::table('change')->insert([
545963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
546963fbaeeSGreg Roach            'xref'       => $xref,
547963fbaeeSGreg Roach            'old_gedcom' => '',
548963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
549963fbaeeSGreg Roach            'user_id'    => Auth::id(),
550afb591d7SGreg Roach        ]);
551304f20d5SGreg Roach
552304f20d5SGreg Roach        // Accept this pending change
5537c4add84SGreg Roach        if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') {
554a091ac74SGreg Roach            $record = Factory::family()->new($xref, $gedcom, null, $this);
555afb591d7SGreg Roach
55622e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
55722e73debSGreg Roach
55822e73debSGreg Roach            return $record;
559304f20d5SGreg Roach        }
560afb591d7SGreg Roach
561a091ac74SGreg Roach        return Factory::family()->new($xref, '', $gedcom, $this);
562afb591d7SGreg Roach    }
563afb591d7SGreg Roach
564afb591d7SGreg Roach    /**
565afb591d7SGreg Roach     * Create a new individual from GEDCOM data.
566afb591d7SGreg Roach     *
567afb591d7SGreg Roach     * @param string $gedcom
568afb591d7SGreg Roach     *
569afb591d7SGreg Roach     * @return Individual
570afb591d7SGreg Roach     * @throws InvalidArgumentException
571afb591d7SGreg Roach     */
572afb591d7SGreg Roach    public function createIndividual(string $gedcom): GedcomRecord
573afb591d7SGreg Roach    {
574bec87e94SGreg Roach        if (!Str::startsWith($gedcom, '0 @@ INDI')) {
575afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI');
576afb591d7SGreg Roach        }
577afb591d7SGreg Roach
578afb591d7SGreg Roach        $xref   = $this->getNewXref();
579bec87e94SGreg Roach        $gedcom = '0 @' . $xref . '@ INDI' . Str::after($gedcom, '0 @@ INDI');
580afb591d7SGreg Roach
581afb591d7SGreg Roach        // Create a change record
58253432476SGreg Roach        $today = strtoupper(date('d M Y'));
58353432476SGreg Roach        $now   = date('H:i:s');
58453432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
585afb591d7SGreg Roach
586afb591d7SGreg Roach        // Create a pending change
587963fbaeeSGreg Roach        DB::table('change')->insert([
588963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
589963fbaeeSGreg Roach            'xref'       => $xref,
590963fbaeeSGreg Roach            'old_gedcom' => '',
591963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
592963fbaeeSGreg Roach            'user_id'    => Auth::id(),
593afb591d7SGreg Roach        ]);
594afb591d7SGreg Roach
595afb591d7SGreg Roach        // Accept this pending change
5967c4add84SGreg Roach        if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') {
597a091ac74SGreg Roach            $record = Factory::individual()->new($xref, $gedcom, null, $this);
598afb591d7SGreg Roach
59922e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
60022e73debSGreg Roach
60122e73debSGreg Roach            return $record;
602afb591d7SGreg Roach        }
603afb591d7SGreg Roach
604a091ac74SGreg Roach        return Factory::individual()->new($xref, '', $gedcom, $this);
605304f20d5SGreg Roach    }
6068586983fSGreg Roach
6078586983fSGreg Roach    /**
60820b58d20SGreg Roach     * Create a new media object from GEDCOM data.
60920b58d20SGreg Roach     *
61020b58d20SGreg Roach     * @param string $gedcom
61120b58d20SGreg Roach     *
61220b58d20SGreg Roach     * @return Media
61320b58d20SGreg Roach     * @throws InvalidArgumentException
61420b58d20SGreg Roach     */
61520b58d20SGreg Roach    public function createMediaObject(string $gedcom): Media
61620b58d20SGreg Roach    {
617bec87e94SGreg Roach        if (!Str::startsWith($gedcom, '0 @@ OBJE')) {
61820b58d20SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE');
61920b58d20SGreg Roach        }
62020b58d20SGreg Roach
62120b58d20SGreg Roach        $xref   = $this->getNewXref();
622bec87e94SGreg Roach        $gedcom = '0 @' . $xref . '@ OBJE' . Str::after($gedcom, '0 @@ OBJE');
62320b58d20SGreg Roach
62420b58d20SGreg Roach        // Create a change record
62553432476SGreg Roach        $today = strtoupper(date('d M Y'));
62653432476SGreg Roach        $now   = date('H:i:s');
62753432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
62820b58d20SGreg Roach
62920b58d20SGreg Roach        // Create a pending change
630963fbaeeSGreg Roach        DB::table('change')->insert([
631963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
632963fbaeeSGreg Roach            'xref'       => $xref,
633963fbaeeSGreg Roach            'old_gedcom' => '',
634963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
635963fbaeeSGreg Roach            'user_id'    => Auth::id(),
63620b58d20SGreg Roach        ]);
63720b58d20SGreg Roach
63820b58d20SGreg Roach        // Accept this pending change
6397c4add84SGreg Roach        if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') {
640a091ac74SGreg Roach            $record = Factory::media()->new($xref, $gedcom, null, $this);
64120b58d20SGreg Roach
64222e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
64322e73debSGreg Roach
64422e73debSGreg Roach            return $record;
64520b58d20SGreg Roach        }
64620b58d20SGreg Roach
647a091ac74SGreg Roach        return Factory::media()->new($xref, '', $gedcom, $this);
64820b58d20SGreg Roach    }
64920b58d20SGreg Roach
65020b58d20SGreg Roach    /**
6518586983fSGreg Roach     * What is the most significant individual in this tree.
6528586983fSGreg Roach     *
653e5a6b4d4SGreg Roach     * @param UserInterface $user
6543370567dSGreg Roach     * @param string        $xref
6558586983fSGreg Roach     *
6568586983fSGreg Roach     * @return Individual
6578586983fSGreg Roach     */
6583370567dSGreg Roach    public function significantIndividual(UserInterface $user, $xref = ''): Individual
659c1010edaSGreg Roach    {
6603370567dSGreg Roach        if ($xref === '') {
6618f9b0fb2SGreg Roach            $individual = null;
6623370567dSGreg Roach        } else {
663a091ac74SGreg Roach            $individual = Factory::individual()->make($xref, $this);
6643370567dSGreg Roach
6653370567dSGreg Roach            if ($individual === null) {
666a091ac74SGreg Roach                $family = Factory::family()->make($xref, $this);
6673370567dSGreg Roach
6683370567dSGreg Roach                if ($family instanceof Family) {
6693370567dSGreg Roach                    $individual = $family->spouses()->first() ?? $family->children()->first();
6703370567dSGreg Roach                }
6713370567dSGreg Roach            }
6723370567dSGreg Roach        }
6738586983fSGreg Roach
6746e91273cSGreg Roach        if ($individual === null && $this->getUserPreference($user, User::PREF_TREE_DEFAULT_XREF) !== '') {
675a091ac74SGreg Roach            $individual = Factory::individual()->make($this->getUserPreference($user, User::PREF_TREE_DEFAULT_XREF), $this);
6768586983fSGreg Roach        }
6778f9b0fb2SGreg Roach
6787c4add84SGreg Roach        if ($individual === null && $this->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF) !== '') {
679a091ac74SGreg Roach            $individual = Factory::individual()->make($this->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF), $this);
6808586983fSGreg Roach        }
6818f9b0fb2SGreg Roach
682bec87e94SGreg Roach        if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') {
683a091ac74SGreg Roach            $individual = Factory::individual()->make($this->getPreference('PEDIGREE_ROOT_ID'), $this);
6848586983fSGreg Roach        }
6858f9b0fb2SGreg Roach        if ($individual === null) {
6868f9b0fb2SGreg Roach            $xref = (string) DB::table('individuals')
6878f9b0fb2SGreg Roach                ->where('i_file', '=', $this->id())
6888f9b0fb2SGreg Roach                ->min('i_id');
689769d7d6eSGreg Roach
690a091ac74SGreg Roach            $individual = Factory::individual()->make($xref, $this);
6915fe1add5SGreg Roach        }
6928f9b0fb2SGreg Roach        if ($individual === null) {
6935fe1add5SGreg Roach            // always return a record
694a091ac74SGreg Roach            $individual = Factory::individual()->new('I', '0 @I@ INDI', null, $this);
6955fe1add5SGreg Roach        }
6965fe1add5SGreg Roach
6975fe1add5SGreg Roach        return $individual;
6985fe1add5SGreg Roach    }
6991df7ae39SGreg Roach
70085a166d8SGreg Roach    /**
70185a166d8SGreg Roach     * Where do we store our media files.
70285a166d8SGreg Roach     *
703a04bb9a2SGreg Roach     * @param FilesystemInterface $data_filesystem
704a04bb9a2SGreg Roach     *
70585a166d8SGreg Roach     * @return FilesystemInterface
70685a166d8SGreg Roach     */
707a04bb9a2SGreg Roach    public function mediaFilesystem(FilesystemInterface $data_filesystem): FilesystemInterface
7081df7ae39SGreg Roach    {
709456d0d35SGreg Roach        $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/');
710a04bb9a2SGreg Roach        $adapter   = new ChrootAdapter($data_filesystem, $media_dir);
711456d0d35SGreg Roach
712456d0d35SGreg Roach        return new Filesystem($adapter);
7131df7ae39SGreg Roach    }
714a25f0a04SGreg Roach}
715