xref: /webtrees/app/Tree.php (revision 2c6f1bd538f46b93645991518398bb087011cb42)
1a25f0a04SGreg Roach<?php
23976b470SGreg Roach
3a25f0a04SGreg Roach/**
4a25f0a04SGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
6a25f0a04SGreg Roach * This program is free software: you can redistribute it and/or modify
7a25f0a04SGreg Roach * it under the terms of the GNU General Public License as published by
8a25f0a04SGreg Roach * the Free Software Foundation, either version 3 of the License, or
9a25f0a04SGreg Roach * (at your option) any later version.
10a25f0a04SGreg Roach * This program is distributed in the hope that it will be useful,
11a25f0a04SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12a25f0a04SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13a25f0a04SGreg Roach * GNU General Public License for more details.
14a25f0a04SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16a25f0a04SGreg Roach */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
2076692c8bSGreg Roachnamespace Fisharebest\Webtrees;
21a25f0a04SGreg Roach
225afbc57aSGreg Roachuse Closure;
23e5a6b4d4SGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface;
2422e73debSGreg Roachuse Fisharebest\Webtrees\Services\PendingChangesService;
25afb591d7SGreg Roachuse InvalidArgumentException;
26f7cf8a15SGreg Roachuse League\Flysystem\FilesystemOperator;
27a25f0a04SGreg Roach
28dec352c1SGreg Roachuse function array_key_exists;
2953432476SGreg Roachuse function date;
30b55cbc6bSGreg Roachuse function is_string;
31dec352c1SGreg Roachuse function str_starts_with;
3253432476SGreg Roachuse function strtoupper;
33dec352c1SGreg Roachuse function substr_replace;
341e653452SGreg Roach
35a25f0a04SGreg Roach/**
3676692c8bSGreg Roach * Provide an interface to the wt_gedcom table.
37a25f0a04SGreg Roach */
38c1010edaSGreg Roachclass Tree
39c1010edaSGreg Roach{
40061b43d7SGreg Roach    private const RESN_PRIVACY = [
41061b43d7SGreg Roach        'none'         => Auth::PRIV_PRIVATE,
42061b43d7SGreg Roach        'privacy'      => Auth::PRIV_USER,
43061b43d7SGreg Roach        'confidential' => Auth::PRIV_NONE,
44061b43d7SGreg Roach        'hidden'       => Auth::PRIV_HIDE,
45061b43d7SGreg Roach    ];
463df1e584SGreg Roach
478a07c98eSGreg Roach
488a07c98eSGreg Roach    // Default values for some tree preferences.
498a07c98eSGreg Roach    protected const DEFAULT_PREFERENCES = [
508a07c98eSGreg Roach        'CALENDAR_FORMAT'              => 'gregorian',
518a07c98eSGreg Roach        'CHART_BOX_TAGS'               => '',
528a07c98eSGreg Roach        'EXPAND_SOURCES'               => '0',
539c7bc1e3SGreg Roach        'FAM_FACTS_QUICK'              => 'ENGA,MARR,DIV',
548a07c98eSGreg Roach        'FORMAT_TEXT'                  => 'markdown',
558a07c98eSGreg Roach        'GEDCOM_MEDIA_PATH'            => '',
568a07c98eSGreg Roach        'GENERATE_UIDS'                => '0',
578a07c98eSGreg Roach        'HIDE_GEDCOM_ERRORS'           => '1',
588a07c98eSGreg Roach        'HIDE_LIVE_PEOPLE'             => '1',
598a07c98eSGreg Roach        'INDI_FACTS_QUICK'             => 'BIRT,BURI,BAPM,CENS,DEAT,OCCU,RESI',
608a07c98eSGreg Roach        'KEEP_ALIVE_YEARS_BIRTH'       => '',
618a07c98eSGreg Roach        'KEEP_ALIVE_YEARS_DEATH'       => '',
628a07c98eSGreg Roach        'LANGUAGE'                     => 'en-US',
638a07c98eSGreg Roach        'MAX_ALIVE_AGE'                => '120',
648a07c98eSGreg Roach        'MEDIA_DIRECTORY'              => 'media/',
659062008fSGreg Roach        'MEDIA_UPLOAD'                 => '1', // Auth::PRIV_USER
668a07c98eSGreg Roach        'META_DESCRIPTION'             => '',
678a07c98eSGreg Roach        'META_TITLE'                   => Webtrees::NAME,
688a07c98eSGreg Roach        'NO_UPDATE_CHAN'               => '0',
698a07c98eSGreg Roach        'PEDIGREE_ROOT_ID'             => '',
708a07c98eSGreg Roach        'QUICK_REQUIRED_FACTS'         => 'BIRT,DEAT',
718a07c98eSGreg Roach        'QUICK_REQUIRED_FAMFACTS'      => 'MARR',
728a07c98eSGreg Roach        'REQUIRE_AUTHENTICATION'       => '0',
738a07c98eSGreg Roach        'SAVE_WATERMARK_IMAGE'         => '0',
748a07c98eSGreg Roach        'SHOW_AGE_DIFF'                => '0',
758a07c98eSGreg Roach        'SHOW_COUNTER'                 => '1',
769062008fSGreg Roach        'SHOW_DEAD_PEOPLE'             => '2', // Auth::PRIV_PRIVATE
778a07c98eSGreg Roach        'SHOW_EST_LIST_DATES'          => '0',
788a07c98eSGreg Roach        'SHOW_FACT_ICONS'              => '1',
798a07c98eSGreg Roach        'SHOW_GEDCOM_RECORD'           => '0',
808a07c98eSGreg Roach        'SHOW_HIGHLIGHT_IMAGES'        => '1',
818a07c98eSGreg Roach        'SHOW_LEVEL2_NOTES'            => '1',
829062008fSGreg Roach        'SHOW_LIVING_NAMES'            => '1', // Auth::PRIV_USER
838a07c98eSGreg Roach        'SHOW_MEDIA_DOWNLOAD'          => '0',
849062008fSGreg Roach        'SHOW_NO_WATERMARK'            => '1', // Auth::PRIV_USER
858a07c98eSGreg Roach        'SHOW_PARENTS_AGE'             => '1',
868a07c98eSGreg Roach        'SHOW_PEDIGREE_PLACES'         => '9',
878a07c98eSGreg Roach        'SHOW_PEDIGREE_PLACES_SUFFIX'  => '0',
888a07c98eSGreg Roach        'SHOW_PRIVATE_RELATIONSHIPS'   => '1',
898a07c98eSGreg Roach        'SHOW_RELATIVES_EVENTS'        => '_BIRT_CHIL,_BIRT_SIBL,_MARR_CHIL,_MARR_PARE,_DEAT_CHIL,_DEAT_PARE,_DEAT_GPAR,_DEAT_SIBL,_DEAT_SPOU',
908a07c98eSGreg Roach        'SUBLIST_TRIGGER_I'            => '200',
918a07c98eSGreg Roach        'SURNAME_LIST_STYLE'           => 'style2',
928a07c98eSGreg Roach        'SURNAME_TRADITION'            => 'paternal',
938a07c98eSGreg Roach        'USE_SILHOUETTE'               => '1',
948a07c98eSGreg Roach        'WORD_WRAPPED_NOTES'           => '0',
958a07c98eSGreg Roach    ];
968a07c98eSGreg Roach
974c78e066SGreg Roach    private int $id;
983df1e584SGreg Roach
994c78e066SGreg Roach    private string $name;
1003df1e584SGreg Roach
1014c78e066SGreg Roach    private string $title;
1023df1e584SGreg Roach
1034c78e066SGreg Roach    /** @var array<int> Default access rules for facts in this tree */
1044c78e066SGreg Roach    private array $fact_privacy;
1053df1e584SGreg Roach
1064c78e066SGreg Roach    /** @var array<int> Default access rules for individuals in this tree */
1074c78e066SGreg Roach    private array $individual_privacy;
1083df1e584SGreg Roach
1094c78e066SGreg Roach    /** @var array<array<int>> Default access rules for individual facts in this tree */
1104c78e066SGreg Roach    private array $individual_fact_privacy;
1113df1e584SGreg Roach
11209482a55SGreg Roach    /** @var array<string> Cached copy of the wt_gedcom_setting table. */
1135f2657cbSGreg Roach    private array $preferences = [];
1143df1e584SGreg Roach
11509482a55SGreg Roach    /** @var array<array<string>> Cached copy of the wt_user_gedcom_setting table. */
1164c78e066SGreg Roach    private array $user_preferences = [];
117061b43d7SGreg Roach
118a25f0a04SGreg Roach    /**
1193df1e584SGreg Roach     * Create a tree object.
120a25f0a04SGreg Roach     *
12172cf66d4SGreg Roach     * @param int    $id
122aa6f03bbSGreg Roach     * @param string $name
123cc13d6d8SGreg Roach     * @param string $title
124a25f0a04SGreg Roach     */
1255afbc57aSGreg Roach    public function __construct(int $id, string $name, string $title)
126c1010edaSGreg Roach    {
12772cf66d4SGreg Roach        $this->id                      = $id;
128aa6f03bbSGreg Roach        $this->name                    = $name;
129cc13d6d8SGreg Roach        $this->title                   = $title;
13013abd6f3SGreg Roach        $this->fact_privacy            = [];
13113abd6f3SGreg Roach        $this->individual_privacy      = [];
13213abd6f3SGreg Roach        $this->individual_fact_privacy = [];
133518bbdc1SGreg Roach
134518bbdc1SGreg Roach        // Load the privacy settings for this tree
135061b43d7SGreg Roach        $rows = DB::table('default_resn')
136061b43d7SGreg Roach            ->where('gedcom_id', '=', $this->id)
137061b43d7SGreg Roach            ->get();
138518bbdc1SGreg Roach
139518bbdc1SGreg Roach        foreach ($rows as $row) {
140061b43d7SGreg Roach            // Convert GEDCOM privacy restriction to a webtrees access level.
141061b43d7SGreg Roach            $row->resn = self::RESN_PRIVACY[$row->resn];
142061b43d7SGreg Roach
143518bbdc1SGreg Roach            if ($row->xref !== null) {
144518bbdc1SGreg Roach                if ($row->tag_type !== null) {
145b262b3d3SGreg Roach                    $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn;
146518bbdc1SGreg Roach                } else {
147b262b3d3SGreg Roach                    $this->individual_privacy[$row->xref] = $row->resn;
148518bbdc1SGreg Roach                }
149518bbdc1SGreg Roach            } else {
150b262b3d3SGreg Roach                $this->fact_privacy[$row->tag_type] = $row->resn;
151518bbdc1SGreg Roach            }
152518bbdc1SGreg Roach        }
153a25f0a04SGreg Roach    }
154a25f0a04SGreg Roach
155a25f0a04SGreg Roach    /**
1565afbc57aSGreg Roach     * A closure which will create a record from a database row.
1575afbc57aSGreg Roach     *
158c6921a17SGreg Roach     * @return Closure(object):Tree
1595afbc57aSGreg Roach     */
1605afbc57aSGreg Roach    public static function rowMapper(): Closure
1615afbc57aSGreg Roach    {
162743491b8SGreg Roach        return static fn (object $row): Tree => new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title);
1635afbc57aSGreg Roach    }
1645afbc57aSGreg Roach
1655afbc57aSGreg Roach    /**
1666ccdf4f0SGreg Roach     * Set the tree’s configuration settings.
1676ccdf4f0SGreg Roach     *
1686ccdf4f0SGreg Roach     * @param string $setting_name
1696ccdf4f0SGreg Roach     * @param string $setting_value
1706ccdf4f0SGreg Roach     *
1716612c384SGreg Roach     * @return self
1726ccdf4f0SGreg Roach     */
1736ccdf4f0SGreg Roach    public function setPreference(string $setting_name, string $setting_value): Tree
1746ccdf4f0SGreg Roach    {
1756ccdf4f0SGreg Roach        if ($setting_value !== $this->getPreference($setting_name)) {
1766ccdf4f0SGreg Roach            DB::table('gedcom_setting')->updateOrInsert([
1776ccdf4f0SGreg Roach                'gedcom_id'    => $this->id,
1786ccdf4f0SGreg Roach                'setting_name' => $setting_name,
1796ccdf4f0SGreg Roach            ], [
1806ccdf4f0SGreg Roach                'setting_value' => $setting_value,
1816ccdf4f0SGreg Roach            ]);
1826ccdf4f0SGreg Roach
1836ccdf4f0SGreg Roach            $this->preferences[$setting_name] = $setting_value;
1846ccdf4f0SGreg Roach
1856ccdf4f0SGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this);
1866ccdf4f0SGreg Roach        }
1876ccdf4f0SGreg Roach
1886ccdf4f0SGreg Roach        return $this;
1896ccdf4f0SGreg Roach    }
1906ccdf4f0SGreg Roach
1916ccdf4f0SGreg Roach    /**
1926ccdf4f0SGreg Roach     * Get the tree’s configuration settings.
1936ccdf4f0SGreg Roach     *
1946ccdf4f0SGreg Roach     * @param string      $setting_name
1959062008fSGreg Roach     * @param string|null $default
1966ccdf4f0SGreg Roach     *
1976ccdf4f0SGreg Roach     * @return string
1986ccdf4f0SGreg Roach     */
199*2c6f1bd5SGreg Roach    public function getPreference(string $setting_name, string|null $default = null): string
2006ccdf4f0SGreg Roach    {
20154c1ab5eSGreg Roach        if ($this->preferences === []) {
2026ccdf4f0SGreg Roach            $this->preferences = DB::table('gedcom_setting')
2036ccdf4f0SGreg Roach                ->where('gedcom_id', '=', $this->id)
2046ccdf4f0SGreg Roach                ->pluck('setting_value', 'setting_name')
2056ccdf4f0SGreg Roach                ->all();
2066ccdf4f0SGreg Roach        }
2076ccdf4f0SGreg Roach
2089062008fSGreg Roach        return $this->preferences[$setting_name] ?? $default ?? self::DEFAULT_PREFERENCES[$setting_name] ?? '';
2096ccdf4f0SGreg Roach    }
2106ccdf4f0SGreg Roach
2116ccdf4f0SGreg Roach    /**
2126ccdf4f0SGreg Roach     * The name of this tree
2136ccdf4f0SGreg Roach     *
2146ccdf4f0SGreg Roach     * @return string
2156ccdf4f0SGreg Roach     */
2166ccdf4f0SGreg Roach    public function name(): string
2176ccdf4f0SGreg Roach    {
2186ccdf4f0SGreg Roach        return $this->name;
2196ccdf4f0SGreg Roach    }
2206ccdf4f0SGreg Roach
2216ccdf4f0SGreg Roach    /**
2226ccdf4f0SGreg Roach     * The title of this tree
2236ccdf4f0SGreg Roach     *
2246ccdf4f0SGreg Roach     * @return string
2256ccdf4f0SGreg Roach     */
2266ccdf4f0SGreg Roach    public function title(): string
2276ccdf4f0SGreg Roach    {
2286ccdf4f0SGreg Roach        return $this->title;
2296ccdf4f0SGreg Roach    }
2306ccdf4f0SGreg Roach
2316ccdf4f0SGreg Roach    /**
2326ccdf4f0SGreg Roach     * The fact-level privacy for this tree.
2336ccdf4f0SGreg Roach     *
23409482a55SGreg Roach     * @return array<int>
2356ccdf4f0SGreg Roach     */
2366ccdf4f0SGreg Roach    public function getFactPrivacy(): array
2376ccdf4f0SGreg Roach    {
2386ccdf4f0SGreg Roach        return $this->fact_privacy;
2396ccdf4f0SGreg Roach    }
2406ccdf4f0SGreg Roach
2416ccdf4f0SGreg Roach    /**
2426ccdf4f0SGreg Roach     * The individual-level privacy for this tree.
2436ccdf4f0SGreg Roach     *
24409482a55SGreg Roach     * @return array<int>
2456ccdf4f0SGreg Roach     */
2466ccdf4f0SGreg Roach    public function getIndividualPrivacy(): array
2476ccdf4f0SGreg Roach    {
2486ccdf4f0SGreg Roach        return $this->individual_privacy;
2496ccdf4f0SGreg Roach    }
2506ccdf4f0SGreg Roach
2516ccdf4f0SGreg Roach    /**
2526ccdf4f0SGreg Roach     * The individual-fact-level privacy for this tree.
2536ccdf4f0SGreg Roach     *
25409482a55SGreg Roach     * @return array<array<int>>
2556ccdf4f0SGreg Roach     */
2566ccdf4f0SGreg Roach    public function getIndividualFactPrivacy(): array
2576ccdf4f0SGreg Roach    {
2586ccdf4f0SGreg Roach        return $this->individual_fact_privacy;
2596ccdf4f0SGreg Roach    }
2606ccdf4f0SGreg Roach
2616ccdf4f0SGreg Roach    /**
2626ccdf4f0SGreg Roach     * Set the tree’s user-configuration settings.
2636ccdf4f0SGreg Roach     *
2646ccdf4f0SGreg Roach     * @param UserInterface $user
2656ccdf4f0SGreg Roach     * @param string        $setting_name
2666ccdf4f0SGreg Roach     * @param string        $setting_value
2676ccdf4f0SGreg Roach     *
2686612c384SGreg Roach     * @return self
2696ccdf4f0SGreg Roach     */
2706ccdf4f0SGreg Roach    public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree
2716ccdf4f0SGreg Roach    {
2726ccdf4f0SGreg Roach        if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
2736ccdf4f0SGreg Roach            // Update the database
2746ccdf4f0SGreg Roach            DB::table('user_gedcom_setting')->updateOrInsert([
2756ccdf4f0SGreg Roach                'gedcom_id'    => $this->id(),
2766ccdf4f0SGreg Roach                'user_id'      => $user->id(),
2776ccdf4f0SGreg Roach                'setting_name' => $setting_name,
2786ccdf4f0SGreg Roach            ], [
2796ccdf4f0SGreg Roach                'setting_value' => $setting_value,
2806ccdf4f0SGreg Roach            ]);
2816ccdf4f0SGreg Roach
2826ccdf4f0SGreg Roach            // Update the cache
2836ccdf4f0SGreg Roach            $this->user_preferences[$user->id()][$setting_name] = $setting_value;
2846ccdf4f0SGreg Roach            // Audit log of changes
2856ccdf4f0SGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this);
2866ccdf4f0SGreg Roach        }
2876ccdf4f0SGreg Roach
2886ccdf4f0SGreg Roach        return $this;
2896ccdf4f0SGreg Roach    }
2906ccdf4f0SGreg Roach
2916ccdf4f0SGreg Roach    /**
2926ccdf4f0SGreg Roach     * Get the tree’s user-configuration settings.
2936ccdf4f0SGreg Roach     *
2946ccdf4f0SGreg Roach     * @param UserInterface $user
2956ccdf4f0SGreg Roach     * @param string        $setting_name
2966ccdf4f0SGreg Roach     * @param string        $default
2976ccdf4f0SGreg Roach     *
2986ccdf4f0SGreg Roach     * @return string
2996ccdf4f0SGreg Roach     */
3006ccdf4f0SGreg Roach    public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string
3016ccdf4f0SGreg Roach    {
3026ccdf4f0SGreg Roach        // There are lots of settings, and we need to fetch lots of them on every page
3036ccdf4f0SGreg Roach        // so it is quicker to fetch them all in one go.
3046ccdf4f0SGreg Roach        if (!array_key_exists($user->id(), $this->user_preferences)) {
3056ccdf4f0SGreg Roach            $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting')
3066ccdf4f0SGreg Roach                ->where('user_id', '=', $user->id())
3076ccdf4f0SGreg Roach                ->where('gedcom_id', '=', $this->id)
3086ccdf4f0SGreg Roach                ->pluck('setting_value', 'setting_name')
3096ccdf4f0SGreg Roach                ->all();
3106ccdf4f0SGreg Roach        }
3116ccdf4f0SGreg Roach
3126ccdf4f0SGreg Roach        return $this->user_preferences[$user->id()][$setting_name] ?? $default;
3136ccdf4f0SGreg Roach    }
3146ccdf4f0SGreg Roach
3156ccdf4f0SGreg Roach    /**
3166ccdf4f0SGreg Roach     * The ID of this tree
3176ccdf4f0SGreg Roach     *
3186ccdf4f0SGreg Roach     * @return int
3196ccdf4f0SGreg Roach     */
3206ccdf4f0SGreg Roach    public function id(): int
3216ccdf4f0SGreg Roach    {
3226ccdf4f0SGreg Roach        return $this->id;
3236ccdf4f0SGreg Roach    }
3246ccdf4f0SGreg Roach
3256ccdf4f0SGreg Roach    /**
3266ccdf4f0SGreg Roach     * Can a user accept changes for this tree?
3276ccdf4f0SGreg Roach     *
3286ccdf4f0SGreg Roach     * @param UserInterface $user
3296ccdf4f0SGreg Roach     *
3306ccdf4f0SGreg Roach     * @return bool
3316ccdf4f0SGreg Roach     */
3326ccdf4f0SGreg Roach    public function canAcceptChanges(UserInterface $user): bool
3336ccdf4f0SGreg Roach    {
3346ccdf4f0SGreg Roach        return Auth::isModerator($this, $user);
3356ccdf4f0SGreg Roach    }
3366ccdf4f0SGreg Roach
3376ccdf4f0SGreg Roach    /**
3380474d79bSGreg Roach     * Are there any pending edits for this tree, that need reviewing by a moderator.
339b78374c5SGreg Roach     *
340b78374c5SGreg Roach     * @return bool
341b78374c5SGreg Roach     */
342771ae10aSGreg Roach    public function hasPendingEdit(): bool
343c1010edaSGreg Roach    {
34415a3f100SGreg Roach        return DB::table('change')
34515a3f100SGreg Roach            ->where('gedcom_id', '=', $this->id)
34615a3f100SGreg Roach            ->where('status', '=', 'pending')
34715a3f100SGreg Roach            ->exists();
348b78374c5SGreg Roach    }
349b78374c5SGreg Roach
350b78374c5SGreg Roach    /**
3516ccdf4f0SGreg Roach     * Create a new record from GEDCOM data.
3526ccdf4f0SGreg Roach     *
3536ccdf4f0SGreg Roach     * @param string $gedcom
3546ccdf4f0SGreg Roach     *
3550d15532eSGreg Roach     * @return GedcomRecord|Individual|Family|Location|Note|Source|Repository|Media|Submitter|Submission
3566ccdf4f0SGreg Roach     * @throws InvalidArgumentException
3576ccdf4f0SGreg Roach     */
3586ccdf4f0SGreg Roach    public function createRecord(string $gedcom): GedcomRecord
3596ccdf4f0SGreg Roach    {
360ef475b14SGreg Roach        if (preg_match('/^0 @@ ([_A-Z]+)/', $gedcom, $match) !== 1) {
3616ccdf4f0SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@');
3626ccdf4f0SGreg Roach        }
3636ccdf4f0SGreg Roach
3646b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make($match[1]);
365dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
3666ccdf4f0SGreg Roach
3676ccdf4f0SGreg Roach        // Create a change record
36853432476SGreg Roach        $today = strtoupper(date('d M Y'));
36953432476SGreg Roach        $now   = date('H:i:s');
37053432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
3716ccdf4f0SGreg Roach
3726ccdf4f0SGreg Roach        // Create a pending change
3736ccdf4f0SGreg Roach        DB::table('change')->insert([
3746ccdf4f0SGreg Roach            'gedcom_id'  => $this->id,
3756ccdf4f0SGreg Roach            'xref'       => $xref,
3766ccdf4f0SGreg Roach            'old_gedcom' => '',
3776ccdf4f0SGreg Roach            'new_gedcom' => $gedcom,
378c3600e89SGreg Roach            'status'     => 'pending',
3796ccdf4f0SGreg Roach            'user_id'    => Auth::id(),
3806ccdf4f0SGreg Roach        ]);
3816ccdf4f0SGreg Roach
3826ccdf4f0SGreg Roach        // Accept this pending change
3831fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
3846b9cb339SGreg Roach            $record = Registry::gedcomRecordFactory()->new($xref, $gedcom, null, $this);
3856ccdf4f0SGreg Roach
386d35568b4SGreg Roach            $pending_changes_service = Registry::container()->get(PendingChangesService::class);
387b55cbc6bSGreg Roach            $pending_changes_service->acceptRecord($record);
38822e73debSGreg Roach
38922e73debSGreg Roach            return $record;
3906ccdf4f0SGreg Roach        }
3916ccdf4f0SGreg Roach
3926b9cb339SGreg Roach        return Registry::gedcomRecordFactory()->new($xref, '', $gedcom, $this);
393a25f0a04SGreg Roach    }
394304f20d5SGreg Roach
395304f20d5SGreg Roach    /**
396afb591d7SGreg Roach     * Create a new family from GEDCOM data.
397afb591d7SGreg Roach     *
398afb591d7SGreg Roach     * @param string $gedcom
399afb591d7SGreg Roach     *
400afb591d7SGreg Roach     * @return Family
401afb591d7SGreg Roach     * @throws InvalidArgumentException
402afb591d7SGreg Roach     */
403afb591d7SGreg Roach    public function createFamily(string $gedcom): GedcomRecord
404afb591d7SGreg Roach    {
405dec352c1SGreg Roach        if (!str_starts_with($gedcom, '0 @@ FAM')) {
406afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM');
407afb591d7SGreg Roach        }
408afb591d7SGreg Roach
4096b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make(Family::RECORD_TYPE);
410dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
411afb591d7SGreg Roach
412afb591d7SGreg Roach        // Create a change record
41353432476SGreg Roach        $today = strtoupper(date('d M Y'));
41453432476SGreg Roach        $now   = date('H:i:s');
41553432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
416afb591d7SGreg Roach
417afb591d7SGreg Roach        // Create a pending change
418963fbaeeSGreg Roach        DB::table('change')->insert([
419963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
420963fbaeeSGreg Roach            'xref'       => $xref,
421963fbaeeSGreg Roach            'old_gedcom' => '',
422963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
423c3600e89SGreg Roach            'status'     => 'pending',
424963fbaeeSGreg Roach            'user_id'    => Auth::id(),
425afb591d7SGreg Roach        ]);
426304f20d5SGreg Roach
427304f20d5SGreg Roach        // Accept this pending change
4281fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
4296b9cb339SGreg Roach            $record = Registry::familyFactory()->new($xref, $gedcom, null, $this);
430afb591d7SGreg Roach
431d35568b4SGreg Roach            $pending_changes_service = Registry::container()->get(PendingChangesService::class);
432b55cbc6bSGreg Roach            $pending_changes_service->acceptRecord($record);
43322e73debSGreg Roach
43422e73debSGreg Roach            return $record;
435304f20d5SGreg Roach        }
436afb591d7SGreg Roach
4376b9cb339SGreg Roach        return Registry::familyFactory()->new($xref, '', $gedcom, $this);
438afb591d7SGreg Roach    }
439afb591d7SGreg Roach
440afb591d7SGreg Roach    /**
441afb591d7SGreg Roach     * Create a new individual from GEDCOM data.
442afb591d7SGreg Roach     *
443afb591d7SGreg Roach     * @param string $gedcom
444afb591d7SGreg Roach     *
445afb591d7SGreg Roach     * @return Individual
446afb591d7SGreg Roach     * @throws InvalidArgumentException
447afb591d7SGreg Roach     */
448afb591d7SGreg Roach    public function createIndividual(string $gedcom): GedcomRecord
449afb591d7SGreg Roach    {
450dec352c1SGreg Roach        if (!str_starts_with($gedcom, '0 @@ INDI')) {
451afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI');
452afb591d7SGreg Roach        }
453afb591d7SGreg Roach
4546b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make(Individual::RECORD_TYPE);
455dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
456afb591d7SGreg Roach
457afb591d7SGreg Roach        // Create a change record
45853432476SGreg Roach        $today = strtoupper(date('d M Y'));
45953432476SGreg Roach        $now   = date('H:i:s');
46053432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
461afb591d7SGreg Roach
462afb591d7SGreg Roach        // Create a pending change
463963fbaeeSGreg Roach        DB::table('change')->insert([
464963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
465963fbaeeSGreg Roach            'xref'       => $xref,
466963fbaeeSGreg Roach            'old_gedcom' => '',
467963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
468c3600e89SGreg Roach            'status'     => 'pending',
469963fbaeeSGreg Roach            'user_id'    => Auth::id(),
470afb591d7SGreg Roach        ]);
471afb591d7SGreg Roach
472afb591d7SGreg Roach        // Accept this pending change
4731fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
4746b9cb339SGreg Roach            $record = Registry::individualFactory()->new($xref, $gedcom, null, $this);
475afb591d7SGreg Roach
476d35568b4SGreg Roach            $pending_changes_service = Registry::container()->get(PendingChangesService::class);
477b55cbc6bSGreg Roach            $pending_changes_service->acceptRecord($record);
47822e73debSGreg Roach
47922e73debSGreg Roach            return $record;
480afb591d7SGreg Roach        }
481afb591d7SGreg Roach
4826b9cb339SGreg Roach        return Registry::individualFactory()->new($xref, '', $gedcom, $this);
483304f20d5SGreg Roach    }
4848586983fSGreg Roach
4858586983fSGreg Roach    /**
48620b58d20SGreg Roach     * Create a new media object from GEDCOM data.
48720b58d20SGreg Roach     *
48820b58d20SGreg Roach     * @param string $gedcom
48920b58d20SGreg Roach     *
49020b58d20SGreg Roach     * @return Media
49120b58d20SGreg Roach     * @throws InvalidArgumentException
49220b58d20SGreg Roach     */
49320b58d20SGreg Roach    public function createMediaObject(string $gedcom): Media
49420b58d20SGreg Roach    {
495dec352c1SGreg Roach        if (!str_starts_with($gedcom, '0 @@ OBJE')) {
49620b58d20SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE');
49720b58d20SGreg Roach        }
49820b58d20SGreg Roach
4996b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make(Media::RECORD_TYPE);
500dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
50120b58d20SGreg Roach
50220b58d20SGreg Roach        // Create a change record
50353432476SGreg Roach        $today = strtoupper(date('d M Y'));
50453432476SGreg Roach        $now   = date('H:i:s');
50553432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
50620b58d20SGreg Roach
50720b58d20SGreg Roach        // Create a pending change
508963fbaeeSGreg Roach        DB::table('change')->insert([
509963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
510963fbaeeSGreg Roach            'xref'       => $xref,
511963fbaeeSGreg Roach            'old_gedcom' => '',
512963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
513c3600e89SGreg Roach            'status'     => 'pending',
514963fbaeeSGreg Roach            'user_id'    => Auth::id(),
51520b58d20SGreg Roach        ]);
51620b58d20SGreg Roach
51720b58d20SGreg Roach        // Accept this pending change
5181fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
5196b9cb339SGreg Roach            $record = Registry::mediaFactory()->new($xref, $gedcom, null, $this);
52020b58d20SGreg Roach
521d35568b4SGreg Roach            $pending_changes_service = Registry::container()->get(PendingChangesService::class);
522b55cbc6bSGreg Roach            $pending_changes_service->acceptRecord($record);
52322e73debSGreg Roach
52422e73debSGreg Roach            return $record;
52520b58d20SGreg Roach        }
52620b58d20SGreg Roach
5276b9cb339SGreg Roach        return Registry::mediaFactory()->new($xref, '', $gedcom, $this);
52820b58d20SGreg Roach    }
52920b58d20SGreg Roach
53020b58d20SGreg Roach    /**
5318586983fSGreg Roach     * What is the most significant individual in this tree.
5328586983fSGreg Roach     *
533e5a6b4d4SGreg Roach     * @param UserInterface $user
5343370567dSGreg Roach     * @param string        $xref
5358586983fSGreg Roach     *
5368586983fSGreg Roach     * @return Individual
5378586983fSGreg Roach     */
53873d58381SGreg Roach    public function significantIndividual(UserInterface $user, string $xref = ''): Individual
539c1010edaSGreg Roach    {
5403370567dSGreg Roach        if ($xref === '') {
5418f9b0fb2SGreg Roach            $individual = null;
5423370567dSGreg Roach        } else {
5436b9cb339SGreg Roach            $individual = Registry::individualFactory()->make($xref, $this);
5443370567dSGreg Roach
5453370567dSGreg Roach            if ($individual === null) {
5466b9cb339SGreg Roach                $family = Registry::familyFactory()->make($xref, $this);
5473370567dSGreg Roach
5483370567dSGreg Roach                if ($family instanceof Family) {
5493370567dSGreg Roach                    $individual = $family->spouses()->first() ?? $family->children()->first();
5503370567dSGreg Roach                }
5513370567dSGreg Roach            }
5523370567dSGreg Roach        }
5538586983fSGreg Roach
5541fe542e9SGreg Roach        if ($individual === null && $this->getUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF) !== '') {
5551fe542e9SGreg Roach            $individual = Registry::individualFactory()->make($this->getUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF), $this);
5568586983fSGreg Roach        }
5578f9b0fb2SGreg Roach
5581fe542e9SGreg Roach        if ($individual === null && $this->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF) !== '') {
5591fe542e9SGreg Roach            $individual = Registry::individualFactory()->make($this->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF), $this);
5608586983fSGreg Roach        }
5618f9b0fb2SGreg Roach
562bec87e94SGreg Roach        if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') {
5636b9cb339SGreg Roach            $individual = Registry::individualFactory()->make($this->getPreference('PEDIGREE_ROOT_ID'), $this);
5648586983fSGreg Roach        }
5658f9b0fb2SGreg Roach        if ($individual === null) {
566b55cbc6bSGreg Roach            $xref = DB::table('individuals')
5678f9b0fb2SGreg Roach                ->where('i_file', '=', $this->id())
5688f9b0fb2SGreg Roach                ->min('i_id');
569769d7d6eSGreg Roach
570b55cbc6bSGreg Roach            if (is_string($xref)) {
5716b9cb339SGreg Roach                $individual = Registry::individualFactory()->make($xref, $this);
5725fe1add5SGreg Roach            }
573b55cbc6bSGreg Roach        }
5748f9b0fb2SGreg Roach        if ($individual === null) {
5755fe1add5SGreg Roach            // always return a record
5766b9cb339SGreg Roach            $individual = Registry::individualFactory()->new('I', '0 @I@ INDI', null, $this);
5775fe1add5SGreg Roach        }
5785fe1add5SGreg Roach
5795fe1add5SGreg Roach        return $individual;
5805fe1add5SGreg Roach    }
5811df7ae39SGreg Roach
58285a166d8SGreg Roach    /**
58385a166d8SGreg Roach     * Where do we store our media files.
58485a166d8SGreg Roach     *
585f7cf8a15SGreg Roach     * @return FilesystemOperator
58685a166d8SGreg Roach     */
5879458f20aSGreg Roach    public function mediaFilesystem(): FilesystemOperator
5881df7ae39SGreg Roach    {
5899458f20aSGreg Roach        return Registry::filesystem()->data($this->getPreference('MEDIA_DIRECTORY'));
5901df7ae39SGreg Roach    }
591a25f0a04SGreg Roach}
592