xref: /webtrees/app/Tree.php (revision 8a07c98e2c323f7fe8deb3ac46485d8082b4b795)
1a25f0a04SGreg Roach<?php
23976b470SGreg Roach
3a25f0a04SGreg Roach/**
4a25f0a04SGreg Roach * webtrees: online genealogy
51fe542e9SGreg Roach * Copyright (C) 2021 webtrees development team
6a25f0a04SGreg Roach * This program is free software: you can redistribute it and/or modify
7a25f0a04SGreg Roach * it under the terms of the GNU General Public License as published by
8a25f0a04SGreg Roach * the Free Software Foundation, either version 3 of the License, or
9a25f0a04SGreg Roach * (at your option) any later version.
10a25f0a04SGreg Roach * This program is distributed in the hope that it will be useful,
11a25f0a04SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
12a25f0a04SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13a25f0a04SGreg Roach * GNU General Public License for more details.
14a25f0a04SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
16a25f0a04SGreg Roach */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
2076692c8bSGreg Roachnamespace Fisharebest\Webtrees;
21a25f0a04SGreg Roach
225afbc57aSGreg Roachuse Closure;
23456d0d35SGreg Roachuse Fisharebest\Flysystem\Adapter\ChrootAdapter;
24e5a6b4d4SGreg Roachuse Fisharebest\Webtrees\Contracts\UserInterface;
2522e73debSGreg Roachuse Fisharebest\Webtrees\Services\PendingChangesService;
2601461f86SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
27afb591d7SGreg Roachuse InvalidArgumentException;
281df7ae39SGreg Roachuse League\Flysystem\Filesystem;
29f7cf8a15SGreg Roachuse League\Flysystem\FilesystemOperator;
308b67c11aSGreg Roachuse stdClass;
31a25f0a04SGreg Roach
321e653452SGreg Roachuse function app;
33dec352c1SGreg Roachuse function array_key_exists;
3453432476SGreg Roachuse function date;
35dec352c1SGreg Roachuse function str_starts_with;
3653432476SGreg Roachuse function strtoupper;
37dec352c1SGreg Roachuse function substr_replace;
381e653452SGreg Roach
39a25f0a04SGreg Roach/**
4076692c8bSGreg Roach * Provide an interface to the wt_gedcom table.
41a25f0a04SGreg Roach */
42c1010edaSGreg Roachclass Tree
43c1010edaSGreg Roach{
44061b43d7SGreg Roach    private const RESN_PRIVACY = [
45061b43d7SGreg Roach        'none'         => Auth::PRIV_PRIVATE,
46061b43d7SGreg Roach        'privacy'      => Auth::PRIV_USER,
47061b43d7SGreg Roach        'confidential' => Auth::PRIV_NONE,
48061b43d7SGreg Roach        'hidden'       => Auth::PRIV_HIDE,
49061b43d7SGreg Roach    ];
503df1e584SGreg Roach
51*8a07c98eSGreg Roach
52*8a07c98eSGreg Roach    // Default values for some tree preferences.
53*8a07c98eSGreg Roach    protected const DEFAULT_PREFERENCES = [
54*8a07c98eSGreg Roach        'ADVANCED_NAME_FACTS'          => 'NICK',
55*8a07c98eSGreg Roach        'ADVANCED_PLAC_FACTS'          => '',
56*8a07c98eSGreg Roach        'CALENDAR_FORMAT'              => 'gregorian',
57*8a07c98eSGreg Roach        'CHART_BOX_TAGS'               => '',
58*8a07c98eSGreg Roach        'EXPAND_SOURCES'               => '0',
59*8a07c98eSGreg Roach        'FORMAT_TEXT'                  => 'markdown',
60*8a07c98eSGreg Roach        'FULL_SOURCES'                 => '0',
61*8a07c98eSGreg Roach        'GEDCOM_MEDIA_PATH'            => '',
62*8a07c98eSGreg Roach        'GENERATE_UIDS'                => '0',
63*8a07c98eSGreg Roach        'HIDE_GEDCOM_ERRORS'           => '1',
64*8a07c98eSGreg Roach        'HIDE_LIVE_PEOPLE'             => '1',
65*8a07c98eSGreg Roach        'INDI_FACTS_ADD'               => 'AFN,BIRT,DEAT,BURI,CREM,ADOP,BAPM,BARM,BASM,BLES,CHRA,CONF,FCOM,ORDN,NATU,EMIG,IMMI,CENS,PROB,WILL,GRAD,RETI,DSCR,EDUC,IDNO,NATI,NCHI,NMR,OCCU,PROP,RELI,RESI,SSN,TITL,BAPL,CONL,ENDL,SLGC,ASSO,RESN',
66*8a07c98eSGreg Roach        'INDI_FACTS_QUICK'             => 'BIRT,BURI,BAPM,CENS,DEAT,OCCU,RESI',
67*8a07c98eSGreg Roach        'INDI_FACTS_UNIQUE'            => '',
68*8a07c98eSGreg Roach        'KEEP_ALIVE_YEARS_BIRTH'       => '',
69*8a07c98eSGreg Roach        'KEEP_ALIVE_YEARS_DEATH'       => '',
70*8a07c98eSGreg Roach        'LANGUAGE'                     => 'en-US',
71*8a07c98eSGreg Roach        'MAX_ALIVE_AGE'                => '120',
72*8a07c98eSGreg Roach        'MEDIA_DIRECTORY'              => 'media/',
73*8a07c98eSGreg Roach        'MEDIA_UPLOAD'                 => Auth::PRIV_USER,
74*8a07c98eSGreg Roach        'META_DESCRIPTION'             => '',
75*8a07c98eSGreg Roach        'META_TITLE'                   => Webtrees::NAME,
76*8a07c98eSGreg Roach        'NO_UPDATE_CHAN'               => '0',
77*8a07c98eSGreg Roach        'PEDIGREE_ROOT_ID'             => '',
78*8a07c98eSGreg Roach        'PREFER_LEVEL2_SOURCES'        => '1',
79*8a07c98eSGreg Roach        'QUICK_REQUIRED_FACTS'         => 'BIRT,DEAT',
80*8a07c98eSGreg Roach        'QUICK_REQUIRED_FAMFACTS'      => 'MARR',
81*8a07c98eSGreg Roach        'REQUIRE_AUTHENTICATION'       => '0',
82*8a07c98eSGreg Roach        'SAVE_WATERMARK_IMAGE'         => '0',
83*8a07c98eSGreg Roach        'SHOW_AGE_DIFF'                => '0',
84*8a07c98eSGreg Roach        'SHOW_COUNTER'                 => '1',
85*8a07c98eSGreg Roach        'SHOW_DEAD_PEOPLE'             => Auth::PRIV_PRIVATE,
86*8a07c98eSGreg Roach        'SHOW_EST_LIST_DATES'          => '0',
87*8a07c98eSGreg Roach        'SHOW_FACT_ICONS'              => '1',
88*8a07c98eSGreg Roach        'SHOW_GEDCOM_RECORD'           => '0',
89*8a07c98eSGreg Roach        'SHOW_HIGHLIGHT_IMAGES'        => '1',
90*8a07c98eSGreg Roach        'SHOW_LEVEL2_NOTES'            => '1',
91*8a07c98eSGreg Roach        'SHOW_LIVING_NAMES'            => Auth::PRIV_USER,
92*8a07c98eSGreg Roach        'SHOW_MEDIA_DOWNLOAD'          => '0',
93*8a07c98eSGreg Roach        'SHOW_NO_WATERMARK'            => Auth::PRIV_USER,
94*8a07c98eSGreg Roach        'SHOW_PARENTS_AGE'             => '1',
95*8a07c98eSGreg Roach        'SHOW_PEDIGREE_PLACES'         => '9',
96*8a07c98eSGreg Roach        'SHOW_PEDIGREE_PLACES_SUFFIX'  => '0',
97*8a07c98eSGreg Roach        'SHOW_PRIVATE_RELATIONSHIPS'   => '1',
98*8a07c98eSGreg Roach        'SHOW_RELATIVES_EVENTS'        => '_BIRT_CHIL,_BIRT_SIBL,_MARR_CHIL,_MARR_PARE,_DEAT_CHIL,_DEAT_PARE,_DEAT_GPAR,_DEAT_SIBL,_DEAT_SPOU',
99*8a07c98eSGreg Roach        'SUBLIST_TRIGGER_I'            => '200',
100*8a07c98eSGreg Roach        'SURNAME_LIST_STYLE'           => 'style2',
101*8a07c98eSGreg Roach        'SURNAME_TRADITION'            => 'paternal',
102*8a07c98eSGreg Roach        'THUMBNAIL_WIDTH'              => '100',
103*8a07c98eSGreg Roach        'USE_SILHOUETTE'               => '1',
104*8a07c98eSGreg Roach        'WORD_WRAPPED_NOTES'           => '0',
105*8a07c98eSGreg Roach    ];
106*8a07c98eSGreg Roach
1076ccdf4f0SGreg Roach    /** @var int The tree's ID number */
1086ccdf4f0SGreg Roach    private $id;
1093df1e584SGreg Roach
1106ccdf4f0SGreg Roach    /** @var string The tree's name */
1116ccdf4f0SGreg Roach    private $name;
1123df1e584SGreg Roach
1136ccdf4f0SGreg Roach    /** @var string The tree's title */
1146ccdf4f0SGreg Roach    private $title;
1153df1e584SGreg Roach
1166ccdf4f0SGreg Roach    /** @var int[] Default access rules for facts in this tree */
1176ccdf4f0SGreg Roach    private $fact_privacy;
1183df1e584SGreg Roach
1196ccdf4f0SGreg Roach    /** @var int[] Default access rules for individuals in this tree */
1206ccdf4f0SGreg Roach    private $individual_privacy;
1213df1e584SGreg Roach
1226ccdf4f0SGreg Roach    /** @var integer[][] Default access rules for individual facts in this tree */
1236ccdf4f0SGreg Roach    private $individual_fact_privacy;
1243df1e584SGreg Roach
1256ccdf4f0SGreg Roach    /** @var string[] Cached copy of the wt_gedcom_setting table. */
1266ccdf4f0SGreg Roach    private $preferences = [];
1273df1e584SGreg Roach
1286ccdf4f0SGreg Roach    /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */
1296ccdf4f0SGreg Roach    private $user_preferences = [];
130061b43d7SGreg Roach
131a25f0a04SGreg Roach    /**
1323df1e584SGreg Roach     * Create a tree object.
133a25f0a04SGreg Roach     *
13472cf66d4SGreg Roach     * @param int    $id
135aa6f03bbSGreg Roach     * @param string $name
136cc13d6d8SGreg Roach     * @param string $title
137a25f0a04SGreg Roach     */
1385afbc57aSGreg Roach    public function __construct(int $id, string $name, string $title)
139c1010edaSGreg Roach    {
14072cf66d4SGreg Roach        $this->id                      = $id;
141aa6f03bbSGreg Roach        $this->name                    = $name;
142cc13d6d8SGreg Roach        $this->title                   = $title;
14313abd6f3SGreg Roach        $this->fact_privacy            = [];
14413abd6f3SGreg Roach        $this->individual_privacy      = [];
14513abd6f3SGreg Roach        $this->individual_fact_privacy = [];
146518bbdc1SGreg Roach
147518bbdc1SGreg Roach        // Load the privacy settings for this tree
148061b43d7SGreg Roach        $rows = DB::table('default_resn')
149061b43d7SGreg Roach            ->where('gedcom_id', '=', $this->id)
150061b43d7SGreg Roach            ->get();
151518bbdc1SGreg Roach
152518bbdc1SGreg Roach        foreach ($rows as $row) {
153061b43d7SGreg Roach            // Convert GEDCOM privacy restriction to a webtrees access level.
154061b43d7SGreg Roach            $row->resn = self::RESN_PRIVACY[$row->resn];
155061b43d7SGreg Roach
156518bbdc1SGreg Roach            if ($row->xref !== null) {
157518bbdc1SGreg Roach                if ($row->tag_type !== null) {
158b262b3d3SGreg Roach                    $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn;
159518bbdc1SGreg Roach                } else {
160b262b3d3SGreg Roach                    $this->individual_privacy[$row->xref] = $row->resn;
161518bbdc1SGreg Roach                }
162518bbdc1SGreg Roach            } else {
163b262b3d3SGreg Roach                $this->fact_privacy[$row->tag_type] = $row->resn;
164518bbdc1SGreg Roach            }
165518bbdc1SGreg Roach        }
166a25f0a04SGreg Roach    }
167a25f0a04SGreg Roach
168a25f0a04SGreg Roach    /**
1695afbc57aSGreg Roach     * A closure which will create a record from a database row.
1705afbc57aSGreg Roach     *
1715afbc57aSGreg Roach     * @return Closure
1725afbc57aSGreg Roach     */
1735afbc57aSGreg Roach    public static function rowMapper(): Closure
1745afbc57aSGreg Roach    {
1755afbc57aSGreg Roach        return static function (stdClass $row): Tree {
1765afbc57aSGreg Roach            return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title);
1775afbc57aSGreg Roach        };
1785afbc57aSGreg Roach    }
1795afbc57aSGreg Roach
1805afbc57aSGreg Roach    /**
1816ccdf4f0SGreg Roach     * Set the tree’s configuration settings.
1826ccdf4f0SGreg Roach     *
1836ccdf4f0SGreg Roach     * @param string $setting_name
1846ccdf4f0SGreg Roach     * @param string $setting_value
1856ccdf4f0SGreg Roach     *
1866ccdf4f0SGreg Roach     * @return $this
1876ccdf4f0SGreg Roach     */
1886ccdf4f0SGreg Roach    public function setPreference(string $setting_name, string $setting_value): Tree
1896ccdf4f0SGreg Roach    {
1906ccdf4f0SGreg Roach        if ($setting_value !== $this->getPreference($setting_name)) {
1916ccdf4f0SGreg Roach            DB::table('gedcom_setting')->updateOrInsert([
1926ccdf4f0SGreg Roach                'gedcom_id'    => $this->id,
1936ccdf4f0SGreg Roach                'setting_name' => $setting_name,
1946ccdf4f0SGreg Roach            ], [
1956ccdf4f0SGreg Roach                'setting_value' => $setting_value,
1966ccdf4f0SGreg Roach            ]);
1976ccdf4f0SGreg Roach
1986ccdf4f0SGreg Roach            $this->preferences[$setting_name] = $setting_value;
1996ccdf4f0SGreg Roach
2006ccdf4f0SGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this);
2016ccdf4f0SGreg Roach        }
2026ccdf4f0SGreg Roach
2036ccdf4f0SGreg Roach        return $this;
2046ccdf4f0SGreg Roach    }
2056ccdf4f0SGreg Roach
2066ccdf4f0SGreg Roach    /**
2076ccdf4f0SGreg Roach     * Get the tree’s configuration settings.
2086ccdf4f0SGreg Roach     *
2096ccdf4f0SGreg Roach     * @param string $setting_name
2106ccdf4f0SGreg Roach     *
2116ccdf4f0SGreg Roach     * @return string
2126ccdf4f0SGreg Roach     */
213*8a07c98eSGreg Roach    public function getPreference(string $setting_name): string
2146ccdf4f0SGreg Roach    {
21554c1ab5eSGreg Roach        if ($this->preferences === []) {
2166ccdf4f0SGreg Roach            $this->preferences = DB::table('gedcom_setting')
2176ccdf4f0SGreg Roach                ->where('gedcom_id', '=', $this->id)
2186ccdf4f0SGreg Roach                ->pluck('setting_value', 'setting_name')
2196ccdf4f0SGreg Roach                ->all();
2206ccdf4f0SGreg Roach        }
2216ccdf4f0SGreg Roach
222*8a07c98eSGreg Roach        return $this->preferences[$setting_name] ?? self::DEFAULT_PREFERENCES[$setting_name] ?? '';
2236ccdf4f0SGreg Roach    }
2246ccdf4f0SGreg Roach
2256ccdf4f0SGreg Roach    /**
2266ccdf4f0SGreg Roach     * The name of this tree
2276ccdf4f0SGreg Roach     *
2286ccdf4f0SGreg Roach     * @return string
2296ccdf4f0SGreg Roach     */
2306ccdf4f0SGreg Roach    public function name(): string
2316ccdf4f0SGreg Roach    {
2326ccdf4f0SGreg Roach        return $this->name;
2336ccdf4f0SGreg Roach    }
2346ccdf4f0SGreg Roach
2356ccdf4f0SGreg Roach    /**
2366ccdf4f0SGreg Roach     * The title of this tree
2376ccdf4f0SGreg Roach     *
2386ccdf4f0SGreg Roach     * @return string
2396ccdf4f0SGreg Roach     */
2406ccdf4f0SGreg Roach    public function title(): string
2416ccdf4f0SGreg Roach    {
2426ccdf4f0SGreg Roach        return $this->title;
2436ccdf4f0SGreg Roach    }
2446ccdf4f0SGreg Roach
2456ccdf4f0SGreg Roach    /**
2466ccdf4f0SGreg Roach     * The fact-level privacy for this tree.
2476ccdf4f0SGreg Roach     *
2486ccdf4f0SGreg Roach     * @return int[]
2496ccdf4f0SGreg Roach     */
2506ccdf4f0SGreg Roach    public function getFactPrivacy(): array
2516ccdf4f0SGreg Roach    {
2526ccdf4f0SGreg Roach        return $this->fact_privacy;
2536ccdf4f0SGreg Roach    }
2546ccdf4f0SGreg Roach
2556ccdf4f0SGreg Roach    /**
2566ccdf4f0SGreg Roach     * The individual-level privacy for this tree.
2576ccdf4f0SGreg Roach     *
2586ccdf4f0SGreg Roach     * @return int[]
2596ccdf4f0SGreg Roach     */
2606ccdf4f0SGreg Roach    public function getIndividualPrivacy(): array
2616ccdf4f0SGreg Roach    {
2626ccdf4f0SGreg Roach        return $this->individual_privacy;
2636ccdf4f0SGreg Roach    }
2646ccdf4f0SGreg Roach
2656ccdf4f0SGreg Roach    /**
2666ccdf4f0SGreg Roach     * The individual-fact-level privacy for this tree.
2676ccdf4f0SGreg Roach     *
2686ccdf4f0SGreg Roach     * @return int[][]
2696ccdf4f0SGreg Roach     */
2706ccdf4f0SGreg Roach    public function getIndividualFactPrivacy(): array
2716ccdf4f0SGreg Roach    {
2726ccdf4f0SGreg Roach        return $this->individual_fact_privacy;
2736ccdf4f0SGreg Roach    }
2746ccdf4f0SGreg Roach
2756ccdf4f0SGreg Roach    /**
2766ccdf4f0SGreg Roach     * Set the tree’s user-configuration settings.
2776ccdf4f0SGreg Roach     *
2786ccdf4f0SGreg Roach     * @param UserInterface $user
2796ccdf4f0SGreg Roach     * @param string        $setting_name
2806ccdf4f0SGreg Roach     * @param string        $setting_value
2816ccdf4f0SGreg Roach     *
2826ccdf4f0SGreg Roach     * @return $this
2836ccdf4f0SGreg Roach     */
2846ccdf4f0SGreg Roach    public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree
2856ccdf4f0SGreg Roach    {
2866ccdf4f0SGreg Roach        if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
2876ccdf4f0SGreg Roach            // Update the database
2886ccdf4f0SGreg Roach            DB::table('user_gedcom_setting')->updateOrInsert([
2896ccdf4f0SGreg Roach                'gedcom_id'    => $this->id(),
2906ccdf4f0SGreg Roach                'user_id'      => $user->id(),
2916ccdf4f0SGreg Roach                'setting_name' => $setting_name,
2926ccdf4f0SGreg Roach            ], [
2936ccdf4f0SGreg Roach                'setting_value' => $setting_value,
2946ccdf4f0SGreg Roach            ]);
2956ccdf4f0SGreg Roach
2966ccdf4f0SGreg Roach            // Update the cache
2976ccdf4f0SGreg Roach            $this->user_preferences[$user->id()][$setting_name] = $setting_value;
2986ccdf4f0SGreg Roach            // Audit log of changes
2996ccdf4f0SGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this);
3006ccdf4f0SGreg Roach        }
3016ccdf4f0SGreg Roach
3026ccdf4f0SGreg Roach        return $this;
3036ccdf4f0SGreg Roach    }
3046ccdf4f0SGreg Roach
3056ccdf4f0SGreg Roach    /**
3066ccdf4f0SGreg Roach     * Get the tree’s user-configuration settings.
3076ccdf4f0SGreg Roach     *
3086ccdf4f0SGreg Roach     * @param UserInterface $user
3096ccdf4f0SGreg Roach     * @param string        $setting_name
3106ccdf4f0SGreg Roach     * @param string        $default
3116ccdf4f0SGreg Roach     *
3126ccdf4f0SGreg Roach     * @return string
3136ccdf4f0SGreg Roach     */
3146ccdf4f0SGreg Roach    public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string
3156ccdf4f0SGreg Roach    {
3166ccdf4f0SGreg Roach        // There are lots of settings, and we need to fetch lots of them on every page
3176ccdf4f0SGreg Roach        // so it is quicker to fetch them all in one go.
3186ccdf4f0SGreg Roach        if (!array_key_exists($user->id(), $this->user_preferences)) {
3196ccdf4f0SGreg Roach            $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting')
3206ccdf4f0SGreg Roach                ->where('user_id', '=', $user->id())
3216ccdf4f0SGreg Roach                ->where('gedcom_id', '=', $this->id)
3226ccdf4f0SGreg Roach                ->pluck('setting_value', 'setting_name')
3236ccdf4f0SGreg Roach                ->all();
3246ccdf4f0SGreg Roach        }
3256ccdf4f0SGreg Roach
3266ccdf4f0SGreg Roach        return $this->user_preferences[$user->id()][$setting_name] ?? $default;
3276ccdf4f0SGreg Roach    }
3286ccdf4f0SGreg Roach
3296ccdf4f0SGreg Roach    /**
3306ccdf4f0SGreg Roach     * The ID of this tree
3316ccdf4f0SGreg Roach     *
3326ccdf4f0SGreg Roach     * @return int
3336ccdf4f0SGreg Roach     */
3346ccdf4f0SGreg Roach    public function id(): int
3356ccdf4f0SGreg Roach    {
3366ccdf4f0SGreg Roach        return $this->id;
3376ccdf4f0SGreg Roach    }
3386ccdf4f0SGreg Roach
3396ccdf4f0SGreg Roach    /**
3406ccdf4f0SGreg Roach     * Can a user accept changes for this tree?
3416ccdf4f0SGreg Roach     *
3426ccdf4f0SGreg Roach     * @param UserInterface $user
3436ccdf4f0SGreg Roach     *
3446ccdf4f0SGreg Roach     * @return bool
3456ccdf4f0SGreg Roach     */
3466ccdf4f0SGreg Roach    public function canAcceptChanges(UserInterface $user): bool
3476ccdf4f0SGreg Roach    {
3486ccdf4f0SGreg Roach        return Auth::isModerator($this, $user);
3496ccdf4f0SGreg Roach    }
3506ccdf4f0SGreg Roach
3516ccdf4f0SGreg Roach    /**
352b78374c5SGreg Roach     * Are there any pending edits for this tree, than need reviewing by a moderator.
353b78374c5SGreg Roach     *
354b78374c5SGreg Roach     * @return bool
355b78374c5SGreg Roach     */
356771ae10aSGreg Roach    public function hasPendingEdit(): bool
357c1010edaSGreg Roach    {
35815a3f100SGreg Roach        return DB::table('change')
35915a3f100SGreg Roach            ->where('gedcom_id', '=', $this->id)
36015a3f100SGreg Roach            ->where('status', '=', 'pending')
36115a3f100SGreg Roach            ->exists();
362b78374c5SGreg Roach    }
363b78374c5SGreg Roach
364b78374c5SGreg Roach    /**
3656ccdf4f0SGreg Roach     * Create a new record from GEDCOM data.
3666ccdf4f0SGreg Roach     *
3676ccdf4f0SGreg Roach     * @param string $gedcom
3686ccdf4f0SGreg Roach     *
3690d15532eSGreg Roach     * @return GedcomRecord|Individual|Family|Location|Note|Source|Repository|Media|Submitter|Submission
3706ccdf4f0SGreg Roach     * @throws InvalidArgumentException
3716ccdf4f0SGreg Roach     */
3726ccdf4f0SGreg Roach    public function createRecord(string $gedcom): GedcomRecord
3736ccdf4f0SGreg Roach    {
374b4a2f885SGreg Roach        if (!preg_match('/^0 @@ ([_A-Z]+)/', $gedcom, $match)) {
3756ccdf4f0SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@');
3766ccdf4f0SGreg Roach        }
3776ccdf4f0SGreg Roach
3786b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make($match[1]);
379dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
3806ccdf4f0SGreg Roach
3816ccdf4f0SGreg Roach        // Create a change record
38253432476SGreg Roach        $today = strtoupper(date('d M Y'));
38353432476SGreg Roach        $now   = date('H:i:s');
38453432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
3856ccdf4f0SGreg Roach
3866ccdf4f0SGreg Roach        // Create a pending change
3876ccdf4f0SGreg Roach        DB::table('change')->insert([
3886ccdf4f0SGreg Roach            'gedcom_id'  => $this->id,
3896ccdf4f0SGreg Roach            'xref'       => $xref,
3906ccdf4f0SGreg Roach            'old_gedcom' => '',
3916ccdf4f0SGreg Roach            'new_gedcom' => $gedcom,
3926ccdf4f0SGreg Roach            'user_id'    => Auth::id(),
3936ccdf4f0SGreg Roach        ]);
3946ccdf4f0SGreg Roach
3956ccdf4f0SGreg Roach        // Accept this pending change
3961fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
3976b9cb339SGreg Roach            $record = Registry::gedcomRecordFactory()->new($xref, $gedcom, null, $this);
3986ccdf4f0SGreg Roach
39922e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
40022e73debSGreg Roach
40122e73debSGreg Roach            return $record;
4026ccdf4f0SGreg Roach        }
4036ccdf4f0SGreg Roach
4046b9cb339SGreg Roach        return Registry::gedcomRecordFactory()->new($xref, '', $gedcom, $this);
405a25f0a04SGreg Roach    }
406304f20d5SGreg Roach
407304f20d5SGreg Roach    /**
408afb591d7SGreg Roach     * Create a new family from GEDCOM data.
409afb591d7SGreg Roach     *
410afb591d7SGreg Roach     * @param string $gedcom
411afb591d7SGreg Roach     *
412afb591d7SGreg Roach     * @return Family
413afb591d7SGreg Roach     * @throws InvalidArgumentException
414afb591d7SGreg Roach     */
415afb591d7SGreg Roach    public function createFamily(string $gedcom): GedcomRecord
416afb591d7SGreg Roach    {
417dec352c1SGreg Roach        if (!str_starts_with($gedcom, '0 @@ FAM')) {
418afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM');
419afb591d7SGreg Roach        }
420afb591d7SGreg Roach
4216b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make(Family::RECORD_TYPE);
422dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
423afb591d7SGreg Roach
424afb591d7SGreg Roach        // Create a change record
42553432476SGreg Roach        $today = strtoupper(date('d M Y'));
42653432476SGreg Roach        $now   = date('H:i:s');
42753432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
428afb591d7SGreg Roach
429afb591d7SGreg Roach        // Create a pending change
430963fbaeeSGreg Roach        DB::table('change')->insert([
431963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
432963fbaeeSGreg Roach            'xref'       => $xref,
433963fbaeeSGreg Roach            'old_gedcom' => '',
434963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
435963fbaeeSGreg Roach            'user_id'    => Auth::id(),
436afb591d7SGreg Roach        ]);
437304f20d5SGreg Roach
438304f20d5SGreg Roach        // Accept this pending change
4391fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
4406b9cb339SGreg Roach            $record = Registry::familyFactory()->new($xref, $gedcom, null, $this);
441afb591d7SGreg Roach
44222e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
44322e73debSGreg Roach
44422e73debSGreg Roach            return $record;
445304f20d5SGreg Roach        }
446afb591d7SGreg Roach
4476b9cb339SGreg Roach        return Registry::familyFactory()->new($xref, '', $gedcom, $this);
448afb591d7SGreg Roach    }
449afb591d7SGreg Roach
450afb591d7SGreg Roach    /**
451afb591d7SGreg Roach     * Create a new individual from GEDCOM data.
452afb591d7SGreg Roach     *
453afb591d7SGreg Roach     * @param string $gedcom
454afb591d7SGreg Roach     *
455afb591d7SGreg Roach     * @return Individual
456afb591d7SGreg Roach     * @throws InvalidArgumentException
457afb591d7SGreg Roach     */
458afb591d7SGreg Roach    public function createIndividual(string $gedcom): GedcomRecord
459afb591d7SGreg Roach    {
460dec352c1SGreg Roach        if (!str_starts_with($gedcom, '0 @@ INDI')) {
461afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI');
462afb591d7SGreg Roach        }
463afb591d7SGreg Roach
4646b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make(Individual::RECORD_TYPE);
465dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
466afb591d7SGreg Roach
467afb591d7SGreg Roach        // Create a change record
46853432476SGreg Roach        $today = strtoupper(date('d M Y'));
46953432476SGreg Roach        $now   = date('H:i:s');
47053432476SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName();
471afb591d7SGreg Roach
472afb591d7SGreg Roach        // Create a pending change
473963fbaeeSGreg Roach        DB::table('change')->insert([
474963fbaeeSGreg Roach            'gedcom_id'  => $this->id,
475963fbaeeSGreg Roach            'xref'       => $xref,
476963fbaeeSGreg Roach            'old_gedcom' => '',
477963fbaeeSGreg Roach            'new_gedcom' => $gedcom,
478963fbaeeSGreg Roach            'user_id'    => Auth::id(),
479afb591d7SGreg Roach        ]);
480afb591d7SGreg Roach
481afb591d7SGreg Roach        // Accept this pending change
4821fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
4836b9cb339SGreg Roach            $record = Registry::individualFactory()->new($xref, $gedcom, null, $this);
484afb591d7SGreg Roach
48522e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
48622e73debSGreg Roach
48722e73debSGreg Roach            return $record;
488afb591d7SGreg Roach        }
489afb591d7SGreg Roach
4906b9cb339SGreg Roach        return Registry::individualFactory()->new($xref, '', $gedcom, $this);
491304f20d5SGreg Roach    }
4928586983fSGreg Roach
4938586983fSGreg Roach    /**
49420b58d20SGreg Roach     * Create a new media object from GEDCOM data.
49520b58d20SGreg Roach     *
49620b58d20SGreg Roach     * @param string $gedcom
49720b58d20SGreg Roach     *
49820b58d20SGreg Roach     * @return Media
49920b58d20SGreg Roach     * @throws InvalidArgumentException
50020b58d20SGreg Roach     */
50120b58d20SGreg Roach    public function createMediaObject(string $gedcom): Media
50220b58d20SGreg Roach    {
503dec352c1SGreg Roach        if (!str_starts_with($gedcom, '0 @@ OBJE')) {
50420b58d20SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE');
50520b58d20SGreg Roach        }
50620b58d20SGreg Roach
5076b9cb339SGreg Roach        $xref   = Registry::xrefFactory()->make(Media::RECORD_TYPE);
508dec352c1SGreg Roach        $gedcom = substr_replace($gedcom, $xref, 3, 0);
50920b58d20SGreg Roach
51020b58d20SGreg 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();
51420b58d20SGreg Roach
51520b58d20SGreg 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(),
52220b58d20SGreg Roach        ]);
52320b58d20SGreg Roach
52420b58d20SGreg Roach        // Accept this pending change
5251fe542e9SGreg Roach        if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') {
5266b9cb339SGreg Roach            $record = Registry::mediaFactory()->new($xref, $gedcom, null, $this);
52720b58d20SGreg Roach
52822e73debSGreg Roach            app(PendingChangesService::class)->acceptRecord($record);
52922e73debSGreg Roach
53022e73debSGreg Roach            return $record;
53120b58d20SGreg Roach        }
53220b58d20SGreg Roach
5336b9cb339SGreg Roach        return Registry::mediaFactory()->new($xref, '', $gedcom, $this);
53420b58d20SGreg Roach    }
53520b58d20SGreg Roach
53620b58d20SGreg Roach    /**
5378586983fSGreg Roach     * What is the most significant individual in this tree.
5388586983fSGreg Roach     *
539e5a6b4d4SGreg Roach     * @param UserInterface $user
5403370567dSGreg Roach     * @param string        $xref
5418586983fSGreg Roach     *
5428586983fSGreg Roach     * @return Individual
5438586983fSGreg Roach     */
5443370567dSGreg Roach    public function significantIndividual(UserInterface $user, $xref = ''): Individual
545c1010edaSGreg Roach    {
5463370567dSGreg Roach        if ($xref === '') {
5478f9b0fb2SGreg Roach            $individual = null;
5483370567dSGreg Roach        } else {
5496b9cb339SGreg Roach            $individual = Registry::individualFactory()->make($xref, $this);
5503370567dSGreg Roach
5513370567dSGreg Roach            if ($individual === null) {
5526b9cb339SGreg Roach                $family = Registry::familyFactory()->make($xref, $this);
5533370567dSGreg Roach
5543370567dSGreg Roach                if ($family instanceof Family) {
5553370567dSGreg Roach                    $individual = $family->spouses()->first() ?? $family->children()->first();
5563370567dSGreg Roach                }
5573370567dSGreg Roach            }
5583370567dSGreg Roach        }
5598586983fSGreg Roach
5601fe542e9SGreg Roach        if ($individual === null && $this->getUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF) !== '') {
5611fe542e9SGreg Roach            $individual = Registry::individualFactory()->make($this->getUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF), $this);
5628586983fSGreg Roach        }
5638f9b0fb2SGreg Roach
5641fe542e9SGreg Roach        if ($individual === null && $this->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF) !== '') {
5651fe542e9SGreg Roach            $individual = Registry::individualFactory()->make($this->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF), $this);
5668586983fSGreg Roach        }
5678f9b0fb2SGreg Roach
568bec87e94SGreg Roach        if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') {
5696b9cb339SGreg Roach            $individual = Registry::individualFactory()->make($this->getPreference('PEDIGREE_ROOT_ID'), $this);
5708586983fSGreg Roach        }
5718f9b0fb2SGreg Roach        if ($individual === null) {
5728f9b0fb2SGreg Roach            $xref = (string) DB::table('individuals')
5738f9b0fb2SGreg Roach                ->where('i_file', '=', $this->id())
5748f9b0fb2SGreg Roach                ->min('i_id');
575769d7d6eSGreg Roach
5766b9cb339SGreg Roach            $individual = Registry::individualFactory()->make($xref, $this);
5775fe1add5SGreg Roach        }
5788f9b0fb2SGreg Roach        if ($individual === null) {
5795fe1add5SGreg Roach            // always return a record
5806b9cb339SGreg Roach            $individual = Registry::individualFactory()->new('I', '0 @I@ INDI', null, $this);
5815fe1add5SGreg Roach        }
5825fe1add5SGreg Roach
5835fe1add5SGreg Roach        return $individual;
5845fe1add5SGreg Roach    }
5851df7ae39SGreg Roach
58685a166d8SGreg Roach    /**
58785a166d8SGreg Roach     * Where do we store our media files.
58885a166d8SGreg Roach     *
589f7cf8a15SGreg Roach     * @param FilesystemOperator $data_filesystem
590a04bb9a2SGreg Roach     *
591f7cf8a15SGreg Roach     * @return FilesystemOperator
59285a166d8SGreg Roach     */
593f7cf8a15SGreg Roach    public function mediaFilesystem(FilesystemOperator $data_filesystem): FilesystemOperator
5941df7ae39SGreg Roach    {
595*8a07c98eSGreg Roach        $media_dir = $this->getPreference('MEDIA_DIRECTORY');
596a04bb9a2SGreg Roach        $adapter   = new ChrootAdapter($data_filesystem, $media_dir);
597456d0d35SGreg Roach
598456d0d35SGreg Roach        return new Filesystem($adapter);
5991df7ae39SGreg Roach    }
600a25f0a04SGreg Roach}
601