xref: /webtrees/app/Tree.php (revision 01461f8661111b35c96e8f511f9f4a2267c68123)
1a25f0a04SGreg Roach<?php
2a25f0a04SGreg Roach/**
3a25f0a04SGreg Roach * webtrees: online genealogy
41062a142SGreg Roach * Copyright (C) 2018 webtrees development team
5a25f0a04SGreg Roach * This program is free software: you can redistribute it and/or modify
6a25f0a04SGreg Roach * it under the terms of the GNU General Public License as published by
7a25f0a04SGreg Roach * the Free Software Foundation, either version 3 of the License, or
8a25f0a04SGreg Roach * (at your option) any later version.
9a25f0a04SGreg Roach * This program is distributed in the hope that it will be useful,
10a25f0a04SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
11a25f0a04SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12a25f0a04SGreg Roach * GNU General Public License for more details.
13a25f0a04SGreg Roach * You should have received a copy of the GNU General Public License
14a25f0a04SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>.
15a25f0a04SGreg Roach */
16e7f56f2aSGreg Roachdeclare(strict_types=1);
17e7f56f2aSGreg Roach
1876692c8bSGreg Roachnamespace Fisharebest\Webtrees;
19a25f0a04SGreg Roach
20b7e60af1SGreg Roachuse Exception;
213d7a8a4cSGreg Roachuse Fisharebest\Webtrees\Functions\FunctionsExport;
223d7a8a4cSGreg Roachuse Fisharebest\Webtrees\Functions\FunctionsImport;
23*01461f86SGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
24*01461f86SGreg Roachuse Illuminate\Database\Query\Builder;
25*01461f86SGreg Roachuse Illuminate\Database\Query\JoinClause;
26afb591d7SGreg Roachuse InvalidArgumentException;
27a25f0a04SGreg Roachuse PDOException;
28afb591d7SGreg Roachuse function substr_compare;
29a25f0a04SGreg Roach
30a25f0a04SGreg Roach/**
3176692c8bSGreg Roach * Provide an interface to the wt_gedcom table.
32a25f0a04SGreg Roach */
33c1010edaSGreg Roachclass Tree
34c1010edaSGreg Roach{
35cbc1590aSGreg Roach    /** @var int The tree's ID number */
3672cf66d4SGreg Roach    private $id;
37518bbdc1SGreg Roach
38a25f0a04SGreg Roach    /** @var string The tree's name */
39a25f0a04SGreg Roach    private $name;
40518bbdc1SGreg Roach
41a25f0a04SGreg Roach    /** @var string The tree's title */
42a25f0a04SGreg Roach    private $title;
43a25f0a04SGreg Roach
44e2052359SGreg Roach    /** @var int[] Default access rules for facts in this tree */
45518bbdc1SGreg Roach    private $fact_privacy;
46518bbdc1SGreg Roach
47e2052359SGreg Roach    /** @var int[] Default access rules for individuals in this tree */
48518bbdc1SGreg Roach    private $individual_privacy;
49518bbdc1SGreg Roach
50518bbdc1SGreg Roach    /** @var integer[][] Default access rules for individual facts in this tree */
51518bbdc1SGreg Roach    private $individual_fact_privacy;
52518bbdc1SGreg Roach
53a25f0a04SGreg Roach    /** @var Tree[] All trees that we have permission to see. */
5475a9f908SGreg Roach    private static $trees = [];
55a25f0a04SGreg Roach
56a25f0a04SGreg Roach    /** @var string[] Cached copy of the wt_gedcom_setting table. */
5715d603e7SGreg Roach    private $preferences = [];
58a25f0a04SGreg Roach
59a25f0a04SGreg Roach    /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */
6013abd6f3SGreg Roach    private $user_preferences = [];
61a25f0a04SGreg Roach
62a25f0a04SGreg Roach    /**
63a25f0a04SGreg Roach     * Create a tree object. This is a private constructor - it can only
64a25f0a04SGreg Roach     * be called from Tree::getAll() to ensure proper initialisation.
65a25f0a04SGreg Roach     *
6672cf66d4SGreg Roach     * @param int    $id
67aa6f03bbSGreg Roach     * @param string $name
68cc13d6d8SGreg Roach     * @param string $title
69a25f0a04SGreg Roach     */
70cc13d6d8SGreg Roach    private function __construct($id, $name, $title)
71c1010edaSGreg Roach    {
7272cf66d4SGreg Roach        $this->id                      = $id;
73aa6f03bbSGreg Roach        $this->name                    = $name;
74cc13d6d8SGreg Roach        $this->title                   = $title;
7513abd6f3SGreg Roach        $this->fact_privacy            = [];
7613abd6f3SGreg Roach        $this->individual_privacy      = [];
7713abd6f3SGreg Roach        $this->individual_fact_privacy = [];
78518bbdc1SGreg Roach
79518bbdc1SGreg Roach        // Load the privacy settings for this tree
80518bbdc1SGreg Roach        $rows = Database::prepare(
81e5588fb0SGreg Roach            "SELECT xref, tag_type, CASE resn WHEN 'none' THEN :priv_public WHEN 'privacy' THEN :priv_user WHEN 'confidential' THEN :priv_none WHEN 'hidden' THEN :priv_hide END AS resn" .
82518bbdc1SGreg Roach            " FROM `##default_resn` WHERE gedcom_id = :tree_id"
8313abd6f3SGreg Roach        )->execute([
844b9ff166SGreg Roach            'priv_public' => Auth::PRIV_PRIVATE,
854b9ff166SGreg Roach            'priv_user'   => Auth::PRIV_USER,
864b9ff166SGreg Roach            'priv_none'   => Auth::PRIV_NONE,
874b9ff166SGreg Roach            'priv_hide'   => Auth::PRIV_HIDE,
8872cf66d4SGreg Roach            'tree_id'     => $this->id,
8913abd6f3SGreg Roach        ])->fetchAll();
90518bbdc1SGreg Roach
91518bbdc1SGreg Roach        foreach ($rows as $row) {
92518bbdc1SGreg Roach            if ($row->xref !== null) {
93518bbdc1SGreg Roach                if ($row->tag_type !== null) {
94518bbdc1SGreg Roach                    $this->individual_fact_privacy[$row->xref][$row->tag_type] = (int) $row->resn;
95518bbdc1SGreg Roach                } else {
96518bbdc1SGreg Roach                    $this->individual_privacy[$row->xref] = (int) $row->resn;
97518bbdc1SGreg Roach                }
98518bbdc1SGreg Roach            } else {
99518bbdc1SGreg Roach                $this->fact_privacy[$row->tag_type] = (int) $row->resn;
100518bbdc1SGreg Roach            }
101518bbdc1SGreg Roach        }
102a25f0a04SGreg Roach    }
103a25f0a04SGreg Roach
104a25f0a04SGreg Roach    /**
105a25f0a04SGreg Roach     * The ID of this tree
106a25f0a04SGreg Roach     *
107cbc1590aSGreg Roach     * @return int
108a25f0a04SGreg Roach     */
10972cf66d4SGreg Roach    public function id(): int
110c1010edaSGreg Roach    {
11172cf66d4SGreg Roach        return $this->id;
112a25f0a04SGreg Roach    }
113a25f0a04SGreg Roach
114a25f0a04SGreg Roach    /**
115a25f0a04SGreg Roach     * The name of this tree
116a25f0a04SGreg Roach     *
117a25f0a04SGreg Roach     * @return string
118a25f0a04SGreg Roach     */
119aa6f03bbSGreg Roach    public function name(): string
120c1010edaSGreg Roach    {
121a25f0a04SGreg Roach        return $this->name;
122a25f0a04SGreg Roach    }
123a25f0a04SGreg Roach
124a25f0a04SGreg Roach    /**
125a25f0a04SGreg Roach     * The title of this tree
126a25f0a04SGreg Roach     *
127a25f0a04SGreg Roach     * @return string
128a25f0a04SGreg Roach     */
129cc13d6d8SGreg Roach    public function title(): string
130c1010edaSGreg Roach    {
131a25f0a04SGreg Roach        return $this->title;
132a25f0a04SGreg Roach    }
133a25f0a04SGreg Roach
134a25f0a04SGreg Roach    /**
135518bbdc1SGreg Roach     * The fact-level privacy for this tree.
136518bbdc1SGreg Roach     *
137e2052359SGreg Roach     * @return int[]
138518bbdc1SGreg Roach     */
1398f53f488SRico Sonntag    public function getFactPrivacy(): array
140c1010edaSGreg Roach    {
141518bbdc1SGreg Roach        return $this->fact_privacy;
142518bbdc1SGreg Roach    }
143518bbdc1SGreg Roach
144518bbdc1SGreg Roach    /**
145518bbdc1SGreg Roach     * The individual-level privacy for this tree.
146518bbdc1SGreg Roach     *
147e2052359SGreg Roach     * @return int[]
148518bbdc1SGreg Roach     */
1498f53f488SRico Sonntag    public function getIndividualPrivacy(): array
150c1010edaSGreg Roach    {
151518bbdc1SGreg Roach        return $this->individual_privacy;
152518bbdc1SGreg Roach    }
153518bbdc1SGreg Roach
154518bbdc1SGreg Roach    /**
155518bbdc1SGreg Roach     * The individual-fact-level privacy for this tree.
156518bbdc1SGreg Roach     *
157adac8af1SGreg Roach     * @return int[][]
158518bbdc1SGreg Roach     */
1598f53f488SRico Sonntag    public function getIndividualFactPrivacy(): array
160c1010edaSGreg Roach    {
161518bbdc1SGreg Roach        return $this->individual_fact_privacy;
162518bbdc1SGreg Roach    }
163518bbdc1SGreg Roach
164518bbdc1SGreg Roach    /**
165a25f0a04SGreg Roach     * Get the tree’s configuration settings.
166a25f0a04SGreg Roach     *
167a25f0a04SGreg Roach     * @param string $setting_name
16815d603e7SGreg Roach     * @param string $default
169a25f0a04SGreg Roach     *
17015d603e7SGreg Roach     * @return string
171a25f0a04SGreg Roach     */
172771ae10aSGreg Roach    public function getPreference(string $setting_name, string $default = ''): string
173c1010edaSGreg Roach    {
17415d603e7SGreg Roach        if (empty($this->preferences)) {
175a25f0a04SGreg Roach            $this->preferences = Database::prepare(
176e5588fb0SGreg Roach                "SELECT setting_name, setting_value FROM `##gedcom_setting` WHERE gedcom_id = ?"
17772cf66d4SGreg Roach            )->execute([$this->id])->fetchAssoc();
178a25f0a04SGreg Roach        }
179a25f0a04SGreg Roach
180b2ce94c6SRico Sonntag        return $this->preferences[$setting_name] ?? $default;
181a25f0a04SGreg Roach    }
182a25f0a04SGreg Roach
183a25f0a04SGreg Roach    /**
184a25f0a04SGreg Roach     * Set the tree’s configuration settings.
185a25f0a04SGreg Roach     *
186a25f0a04SGreg Roach     * @param string $setting_name
187a25f0a04SGreg Roach     * @param string $setting_value
188a25f0a04SGreg Roach     *
189a25f0a04SGreg Roach     * @return $this
190a25f0a04SGreg Roach     */
191771ae10aSGreg Roach    public function setPreference(string $setting_name, string $setting_value): Tree
192c1010edaSGreg Roach    {
193a25f0a04SGreg Roach        if ($setting_value !== $this->getPreference($setting_name)) {
194a25f0a04SGreg Roach            Database::prepare(
195a25f0a04SGreg Roach                "REPLACE INTO `##gedcom_setting` (gedcom_id, setting_name, setting_value)" .
196a25f0a04SGreg Roach                " VALUES (:tree_id, :setting_name, LEFT(:setting_value, 255))"
19713abd6f3SGreg Roach            )->execute([
19872cf66d4SGreg Roach                'tree_id'       => $this->id,
199a25f0a04SGreg Roach                'setting_name'  => $setting_name,
200a25f0a04SGreg Roach                'setting_value' => $setting_value,
20113abd6f3SGreg Roach            ]);
20215d603e7SGreg Roach
203a25f0a04SGreg Roach            $this->preferences[$setting_name] = $setting_value;
20415d603e7SGreg Roach
20572292b7dSGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this);
206a25f0a04SGreg Roach        }
207a25f0a04SGreg Roach
208a25f0a04SGreg Roach        return $this;
209a25f0a04SGreg Roach    }
210a25f0a04SGreg Roach
211a25f0a04SGreg Roach    /**
212a25f0a04SGreg Roach     * Get the tree’s user-configuration settings.
213a25f0a04SGreg Roach     *
214a25f0a04SGreg Roach     * @param User   $user
215a25f0a04SGreg Roach     * @param string $setting_name
2167015ba1fSGreg Roach     * @param string $default
217a25f0a04SGreg Roach     *
218a25f0a04SGreg Roach     * @return string
219a25f0a04SGreg Roach     */
220771ae10aSGreg Roach    public function getUserPreference(User $user, string $setting_name, string $default = ''): string
221c1010edaSGreg Roach    {
222a25f0a04SGreg Roach        // There are lots of settings, and we need to fetch lots of them on every page
223a25f0a04SGreg Roach        // so it is quicker to fetch them all in one go.
224a25f0a04SGreg Roach        if (!array_key_exists($user->getUserId(), $this->user_preferences)) {
225a25f0a04SGreg Roach            $this->user_preferences[$user->getUserId()] = Database::prepare(
226e5588fb0SGreg Roach                "SELECT setting_name, setting_value FROM `##user_gedcom_setting` WHERE user_id = ? AND gedcom_id = ?"
227c1010edaSGreg Roach            )->execute([
228c1010edaSGreg Roach                $user->getUserId(),
22972cf66d4SGreg Roach                $this->id,
230c1010edaSGreg Roach            ])->fetchAssoc();
231a25f0a04SGreg Roach        }
232a25f0a04SGreg Roach
233b2ce94c6SRico Sonntag        return $this->user_preferences[$user->getUserId()][$setting_name] ?? $default;
234a25f0a04SGreg Roach    }
235a25f0a04SGreg Roach
236a25f0a04SGreg Roach    /**
237a25f0a04SGreg Roach     * Set the tree’s user-configuration settings.
238a25f0a04SGreg Roach     *
239a25f0a04SGreg Roach     * @param User   $user
240a25f0a04SGreg Roach     * @param string $setting_name
241a25f0a04SGreg Roach     * @param string $setting_value
242a25f0a04SGreg Roach     *
243a25f0a04SGreg Roach     * @return $this
244a25f0a04SGreg Roach     */
245771ae10aSGreg Roach    public function setUserPreference(User $user, string $setting_name, string $setting_value): Tree
246c1010edaSGreg Roach    {
247a25f0a04SGreg Roach        if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
248a25f0a04SGreg Roach            // Update the database
2497015ba1fSGreg Roach            if ($setting_value === '') {
250a25f0a04SGreg Roach                Database::prepare(
251a25f0a04SGreg Roach                    "DELETE FROM `##user_gedcom_setting` WHERE gedcom_id = :tree_id AND user_id = :user_id AND setting_name = :setting_name"
25213abd6f3SGreg Roach                )->execute([
25372cf66d4SGreg Roach                    'tree_id'      => $this->id,
254a25f0a04SGreg Roach                    'user_id'      => $user->getUserId(),
255a25f0a04SGreg Roach                    'setting_name' => $setting_name,
25613abd6f3SGreg Roach                ]);
257a25f0a04SGreg Roach            } else {
258a25f0a04SGreg Roach                Database::prepare(
259a25f0a04SGreg Roach                    "REPLACE INTO `##user_gedcom_setting` (user_id, gedcom_id, setting_name, setting_value) VALUES (:user_id, :tree_id, :setting_name, LEFT(:setting_value, 255))"
26013abd6f3SGreg Roach                )->execute([
261a25f0a04SGreg Roach                    'user_id'       => $user->getUserId(),
26272cf66d4SGreg Roach                    'tree_id'       => $this->id,
263a25f0a04SGreg Roach                    'setting_name'  => $setting_name,
264cbc1590aSGreg Roach                    'setting_value' => $setting_value,
26513abd6f3SGreg Roach                ]);
266a25f0a04SGreg Roach            }
267a25f0a04SGreg Roach            // Update our cache
268a25f0a04SGreg Roach            $this->user_preferences[$user->getUserId()][$setting_name] = $setting_value;
269a25f0a04SGreg Roach            // Audit log of changes
27072292b7dSGreg Roach            Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->getUserName() . '"', $this);
271a25f0a04SGreg Roach        }
272a25f0a04SGreg Roach
273a25f0a04SGreg Roach        return $this;
274a25f0a04SGreg Roach    }
275a25f0a04SGreg Roach
276a25f0a04SGreg Roach    /**
277a25f0a04SGreg Roach     * Can a user accept changes for this tree?
278a25f0a04SGreg Roach     *
279a25f0a04SGreg Roach     * @param User $user
280a25f0a04SGreg Roach     *
281cbc1590aSGreg Roach     * @return bool
282a25f0a04SGreg Roach     */
283771ae10aSGreg Roach    public function canAcceptChanges(User $user): bool
284c1010edaSGreg Roach    {
285a25f0a04SGreg Roach        return Auth::isModerator($this, $user);
286a25f0a04SGreg Roach    }
287a25f0a04SGreg Roach
288a25f0a04SGreg Roach    /**
289a25f0a04SGreg Roach     * Fetch all the trees that we have permission to access.
290a25f0a04SGreg Roach     *
291a25f0a04SGreg Roach     * @return Tree[]
292a25f0a04SGreg Roach     */
293771ae10aSGreg Roach    public static function getAll(): array
294c1010edaSGreg Roach    {
29575a9f908SGreg Roach        if (empty(self::$trees)) {
296a25f0a04SGreg Roach            $rows = Database::prepare(
297e5588fb0SGreg Roach                "SELECT g.gedcom_id AS tree_id, g.gedcom_name AS tree_name, gs1.setting_value AS tree_title" .
298a25f0a04SGreg Roach                " FROM `##gedcom` g" .
299a25f0a04SGreg Roach                " LEFT JOIN `##gedcom_setting`      gs1 ON (g.gedcom_id=gs1.gedcom_id AND gs1.setting_name='title')" .
300a25f0a04SGreg Roach                " LEFT JOIN `##gedcom_setting`      gs2 ON (g.gedcom_id=gs2.gedcom_id AND gs2.setting_name='imported')" .
301a25f0a04SGreg Roach                " LEFT JOIN `##gedcom_setting`      gs3 ON (g.gedcom_id=gs3.gedcom_id AND gs3.setting_name='REQUIRE_AUTHENTICATION')" .
302a25f0a04SGreg Roach                " LEFT JOIN `##user_gedcom_setting` ugs ON (g.gedcom_id=ugs.gedcom_id AND ugs.setting_name='canedit' AND ugs.user_id=?)" .
303a25f0a04SGreg Roach                " WHERE " .
304a25f0a04SGreg Roach                "  g.gedcom_id>0 AND (" . // exclude the "template" tree
305a25f0a04SGreg Roach                "    EXISTS (SELECT 1 FROM `##user_setting` WHERE user_id=? AND setting_name='canadmin' AND setting_value=1)" . // Admin sees all
306a25f0a04SGreg Roach                "   ) OR (" .
3078cca4ddeSGreg Roach                "    (gs2.setting_value = 1 OR ugs.setting_value = 'admin') AND (" . // Allow imported trees, with either:
308a25f0a04SGreg Roach                "     gs3.setting_value <> 1 OR" . // visitor access
309a25f0a04SGreg Roach                "     IFNULL(ugs.setting_value, 'none')<>'none'" . // explicit access
310a25f0a04SGreg Roach                "   )" .
311a25f0a04SGreg Roach                "  )" .
312a25f0a04SGreg Roach                " ORDER BY g.sort_order, 3"
313c1010edaSGreg Roach            )->execute([
314c1010edaSGreg Roach                Auth::id(),
315c1010edaSGreg Roach                Auth::id(),
316c1010edaSGreg Roach            ])->fetchAll();
31775a9f908SGreg Roach
318*01461f86SGreg Roach            // Admins see all trees
319*01461f86SGreg Roach            /*
320*01461f86SGreg Roach            $query = DB::table('gedcom')
321*01461f86SGreg Roach                ->leftJoin('gedcom_setting', function (JoinClause $join): void {
322*01461f86SGreg Roach                    $join->on('gedcom_setting.gedcom_id', '=', 'gedcom.gedcom_id')
323*01461f86SGreg Roach                        ->where('gedcom_setting.setting_name', '=', 'title');
324*01461f86SGreg Roach                })
325*01461f86SGreg Roach                ->where('gedcom.gedcom_id', '>', 0)
326*01461f86SGreg Roach                ->select([
327*01461f86SGreg Roach                    'gedcom.gedcom_id AS tree_id',
328*01461f86SGreg Roach                    'gedcom.gedcom_name AS tree_name',
329*01461f86SGreg Roach                    'gedcom_setting.setting_value AS tree_title',
330*01461f86SGreg Roach                ])
331*01461f86SGreg Roach                ->orderBy('gedcom.sort_order')
332*01461f86SGreg Roach                ->orderBy('gedcom_setting.setting_value');
333*01461f86SGreg Roach
334*01461f86SGreg Roach            if (!Auth::isAdmin()) {
335*01461f86SGreg Roach                $query
336*01461f86SGreg Roach                    ->join('gedcom_setting AS gs2','gs2.gedcom_id', '=', 'gedcom.gedcom_id')
337*01461f86SGreg Roach                    ->where('gs2.setting_name', '=', 'imported');
338*01461f86SGreg Roach
339*01461f86SGreg Roach                // Some trees are private
340*01461f86SGreg Roach                $query
341*01461f86SGreg Roach                    ->leftJoin('gedcom_setting AS gs3', function (JoinClause $join): void {
342*01461f86SGreg Roach                        $join->on('gs3.gedcom_id', '=', 'gedcom.gedcom_id')
343*01461f86SGreg Roach                            ->where('gs3.setting_name', '=', 'REQUIRE_AUTHENTICATION');
344*01461f86SGreg Roach                    })
345*01461f86SGreg Roach                    ->leftJoin('user_gedcom_setting', function (JoinClause $join): void {
346*01461f86SGreg Roach                        $join->on('user_gedcom_setting.gedcom_id', '=', 'gedcom.gedcom_id')
347*01461f86SGreg Roach                            ->where('user_gedcom_setting.user_id', '=', Auth::id())
348*01461f86SGreg Roach                            ->where('user_gedcom_setting.setting_name', '=', 'canedit');
349*01461f86SGreg Roach                    })
350*01461f86SGreg Roach                    ->where(function (Builder $query): void {
351*01461f86SGreg Roach                        $query
352*01461f86SGreg Roach                            // Managers
353*01461f86SGreg Roach                            ->where('user_gedcom_setting.setting_value', '=', 'admin')
354*01461f86SGreg Roach                            // Members
355*01461f86SGreg Roach                            ->orWhere(function (Builder $query): void {
356*01461f86SGreg Roach                                $query
357*01461f86SGreg Roach                                    ->where('gs2.setting_value', '=', '1')
358*01461f86SGreg Roach                                    ->where('gs3.setting_value', '=', '1')
359*01461f86SGreg Roach                                    ->where('user_gedcom_setting.setting_value', '<>', 'none');
360*01461f86SGreg Roach                            })
361*01461f86SGreg Roach                            // Visitors
362*01461f86SGreg Roach                            ->orWhere(function (Builder $query): void {
363*01461f86SGreg Roach                                $query
364*01461f86SGreg Roach                                    ->where('gs2.setting_value', '=', '1')
365*01461f86SGreg Roach                                    ->where(DB::raw("COALESCE(gs3.setting_value ,'0')"), '=', 0);
366*01461f86SGreg Roach                            });
367*01461f86SGreg Roach                    });
368*01461f86SGreg Roach            }
369*01461f86SGreg Roach
370*01461f86SGreg Roach            $rows = $query->get();
371*01461f86SGreg Roach            */
372*01461f86SGreg Roach
373a25f0a04SGreg Roach            foreach ($rows as $row) {
3743f7ece23SGreg Roach                self::$trees[$row->tree_name] = new self((int) $row->tree_id, $row->tree_name, $row->tree_title);
375a25f0a04SGreg Roach            }
376a25f0a04SGreg Roach        }
377a25f0a04SGreg Roach
378a25f0a04SGreg Roach        return self::$trees;
379a25f0a04SGreg Roach    }
380a25f0a04SGreg Roach
381a25f0a04SGreg Roach    /**
382d2cdeb3fSGreg Roach     * Find the tree with a specific ID.
383a25f0a04SGreg Roach     *
384cbc1590aSGreg Roach     * @param int $tree_id
385cbc1590aSGreg Roach     *
386cbc1590aSGreg Roach     * @throws \DomainException
387a25f0a04SGreg Roach     * @return Tree
388a25f0a04SGreg Roach     */
389771ae10aSGreg Roach    public static function findById($tree_id): Tree
390c1010edaSGreg Roach    {
39151d0f842SGreg Roach        foreach (self::getAll() as $tree) {
39272cf66d4SGreg Roach            if ($tree->id == $tree_id) {
39351d0f842SGreg Roach                return $tree;
39451d0f842SGreg Roach            }
39551d0f842SGreg Roach        }
39659f2f229SGreg Roach        throw new \DomainException();
397a25f0a04SGreg Roach    }
398a25f0a04SGreg Roach
399a25f0a04SGreg Roach    /**
400d2cdeb3fSGreg Roach     * Find the tree with a specific name.
401cf4bcc09SGreg Roach     *
402cf4bcc09SGreg Roach     * @param string $tree_name
403cf4bcc09SGreg Roach     *
404cf4bcc09SGreg Roach     * @return Tree|null
405cf4bcc09SGreg Roach     */
406c1010edaSGreg Roach    public static function findByName($tree_name)
407c1010edaSGreg Roach    {
408cf4bcc09SGreg Roach        foreach (self::getAll() as $tree) {
40951d0f842SGreg Roach            if ($tree->name === $tree_name) {
410cf4bcc09SGreg Roach                return $tree;
411cf4bcc09SGreg Roach            }
412cf4bcc09SGreg Roach        }
413cf4bcc09SGreg Roach
414cf4bcc09SGreg Roach        return null;
415cf4bcc09SGreg Roach    }
416cf4bcc09SGreg Roach
417cf4bcc09SGreg Roach    /**
418a25f0a04SGreg Roach     * Create arguments to select_edit_control()
419a25f0a04SGreg Roach     * Note - these will be escaped later
420a25f0a04SGreg Roach     *
421a25f0a04SGreg Roach     * @return string[]
422a25f0a04SGreg Roach     */
423771ae10aSGreg Roach    public static function getIdList(): array
424c1010edaSGreg Roach    {
42513abd6f3SGreg Roach        $list = [];
426a25f0a04SGreg Roach        foreach (self::getAll() as $tree) {
42772cf66d4SGreg Roach            $list[$tree->id] = $tree->title;
428a25f0a04SGreg Roach        }
429a25f0a04SGreg Roach
430a25f0a04SGreg Roach        return $list;
431a25f0a04SGreg Roach    }
432a25f0a04SGreg Roach
433a25f0a04SGreg Roach    /**
434a25f0a04SGreg Roach     * Create arguments to select_edit_control()
435a25f0a04SGreg Roach     * Note - these will be escaped later
436a25f0a04SGreg Roach     *
437a25f0a04SGreg Roach     * @return string[]
438a25f0a04SGreg Roach     */
439771ae10aSGreg Roach    public static function getNameList(): array
440c1010edaSGreg Roach    {
44113abd6f3SGreg Roach        $list = [];
442a25f0a04SGreg Roach        foreach (self::getAll() as $tree) {
443a25f0a04SGreg Roach            $list[$tree->name] = $tree->title;
444a25f0a04SGreg Roach        }
445a25f0a04SGreg Roach
446a25f0a04SGreg Roach        return $list;
447a25f0a04SGreg Roach    }
448a25f0a04SGreg Roach
449a25f0a04SGreg Roach    /**
450a25f0a04SGreg Roach     * Create a new tree
451a25f0a04SGreg Roach     *
452a25f0a04SGreg Roach     * @param string $tree_name
453a25f0a04SGreg Roach     * @param string $tree_title
454a25f0a04SGreg Roach     *
455a25f0a04SGreg Roach     * @return Tree
456a25f0a04SGreg Roach     */
457771ae10aSGreg Roach    public static function create(string $tree_name, string $tree_title): Tree
458c1010edaSGreg Roach    {
459a25f0a04SGreg Roach        try {
460a25f0a04SGreg Roach            // Create a new tree
461*01461f86SGreg Roach            DB::table('gedcom')->insert([
462*01461f86SGreg Roach                'gedcom_name' => $tree_name,
463*01461f86SGreg Roach            ]);
4644a86d714SGreg Roach
465*01461f86SGreg Roach            $tree_id = DB::connection()->getPdo()->lastInsertId();
466a25f0a04SGreg Roach        } catch (PDOException $ex) {
467a25f0a04SGreg Roach            // A tree with that name already exists?
468ef2fd529SGreg Roach            return self::findByName($tree_name);
469a25f0a04SGreg Roach        }
470a25f0a04SGreg Roach
471a25f0a04SGreg Roach        // Update the list of trees - to include this new one
47275a9f908SGreg Roach        self::$trees = [];
473d2cdeb3fSGreg Roach        $tree        = self::findById($tree_id);
474a25f0a04SGreg Roach
475a25f0a04SGreg Roach        $tree->setPreference('imported', '0');
476a25f0a04SGreg Roach        $tree->setPreference('title', $tree_title);
477a25f0a04SGreg Roach
478a25f0a04SGreg Roach        // Module privacy
479a25f0a04SGreg Roach        Module::setDefaultAccess($tree_id);
480a25f0a04SGreg Roach
4811507cbcaSGreg Roach        // Set preferences from default tree
4821507cbcaSGreg Roach        Database::prepare(
4831507cbcaSGreg Roach            "INSERT INTO `##gedcom_setting` (gedcom_id, setting_name, setting_value)" .
4841507cbcaSGreg Roach            " SELECT :tree_id, setting_name, setting_value" .
4851507cbcaSGreg Roach            " FROM `##gedcom_setting` WHERE gedcom_id = -1"
48613abd6f3SGreg Roach        )->execute([
4871507cbcaSGreg Roach            'tree_id' => $tree_id,
48813abd6f3SGreg Roach        ]);
4891507cbcaSGreg Roach
4901507cbcaSGreg Roach        Database::prepare(
4911507cbcaSGreg Roach            "INSERT INTO `##default_resn` (gedcom_id, tag_type, resn)" .
4921507cbcaSGreg Roach            " SELECT :tree_id, tag_type, resn" .
4931507cbcaSGreg Roach            " FROM `##default_resn` WHERE gedcom_id = -1"
49413abd6f3SGreg Roach        )->execute([
4951507cbcaSGreg Roach            'tree_id' => $tree_id,
49613abd6f3SGreg Roach        ]);
4971507cbcaSGreg Roach
4981507cbcaSGreg Roach        Database::prepare(
4991507cbcaSGreg Roach            "INSERT INTO `##block` (gedcom_id, location, block_order, module_name)" .
5001507cbcaSGreg Roach            " SELECT :tree_id, location, block_order, module_name" .
5011507cbcaSGreg Roach            " FROM `##block` WHERE gedcom_id = -1"
50213abd6f3SGreg Roach        )->execute([
5031507cbcaSGreg Roach            'tree_id' => $tree_id,
50413abd6f3SGreg Roach        ]);
5051507cbcaSGreg Roach
506a25f0a04SGreg Roach        // Gedcom and privacy settings
50776f666f4SGreg Roach        $tree->setPreference('CONTACT_USER_ID', (string) Auth::id());
50876f666f4SGreg Roach        $tree->setPreference('WEBMASTER_USER_ID', (string) Auth::id());
509a25f0a04SGreg Roach        $tree->setPreference('LANGUAGE', WT_LOCALE); // Default to the current admin’s language
510a25f0a04SGreg Roach        switch (WT_LOCALE) {
511a25f0a04SGreg Roach            case 'es':
512a25f0a04SGreg Roach                $tree->setPreference('SURNAME_TRADITION', 'spanish');
513a25f0a04SGreg Roach                break;
514a25f0a04SGreg Roach            case 'is':
515a25f0a04SGreg Roach                $tree->setPreference('SURNAME_TRADITION', 'icelandic');
516a25f0a04SGreg Roach                break;
517a25f0a04SGreg Roach            case 'lt':
518a25f0a04SGreg Roach                $tree->setPreference('SURNAME_TRADITION', 'lithuanian');
519a25f0a04SGreg Roach                break;
520a25f0a04SGreg Roach            case 'pl':
521a25f0a04SGreg Roach                $tree->setPreference('SURNAME_TRADITION', 'polish');
522a25f0a04SGreg Roach                break;
523a25f0a04SGreg Roach            case 'pt':
524a25f0a04SGreg Roach            case 'pt-BR':
525a25f0a04SGreg Roach                $tree->setPreference('SURNAME_TRADITION', 'portuguese');
526a25f0a04SGreg Roach                break;
527a25f0a04SGreg Roach            default:
528a25f0a04SGreg Roach                $tree->setPreference('SURNAME_TRADITION', 'paternal');
529a25f0a04SGreg Roach                break;
530a25f0a04SGreg Roach        }
531a25f0a04SGreg Roach
532a25f0a04SGreg Roach        // Genealogy data
533a25f0a04SGreg Roach        // It is simpler to create a temporary/unimported GEDCOM than to populate all the tables...
534bbb76c12SGreg Roach        /* I18N: This should be a common/default/placeholder name of an individual. Put slashes around the surname. */
535bbb76c12SGreg Roach        $john_doe = I18N::translate('John /DOE/');
53677e70a22SGreg Roach        $note     = I18N::translate('Edit this individual and replace their details with your own.');
53713abd6f3SGreg Roach        Database::prepare("INSERT INTO `##gedcom_chunk` (gedcom_id, chunk_data) VALUES (?, ?)")->execute([
538a25f0a04SGreg Roach            $tree_id,
539cbc1590aSGreg Roach            "0 HEAD\n1 CHAR UTF-8\n0 @I1@ INDI\n1 NAME {$john_doe}\n1 SEX M\n1 BIRT\n2 DATE 01 JAN 1850\n2 NOTE {$note}\n0 TRLR\n",
54013abd6f3SGreg Roach        ]);
541a25f0a04SGreg Roach
542a25f0a04SGreg Roach        // Update our cache
54372cf66d4SGreg Roach        self::$trees[$tree->id] = $tree;
544a25f0a04SGreg Roach
545a25f0a04SGreg Roach        return $tree;
546a25f0a04SGreg Roach    }
547a25f0a04SGreg Roach
548a25f0a04SGreg Roach    /**
549b78374c5SGreg Roach     * Are there any pending edits for this tree, than need reviewing by a moderator.
550b78374c5SGreg Roach     *
551b78374c5SGreg Roach     * @return bool
552b78374c5SGreg Roach     */
553771ae10aSGreg Roach    public function hasPendingEdit(): bool
554c1010edaSGreg Roach    {
555b78374c5SGreg Roach        return (bool) Database::prepare(
556b78374c5SGreg Roach            "SELECT 1 FROM `##change` WHERE status = 'pending' AND gedcom_id = :tree_id"
55713abd6f3SGreg Roach        )->execute([
55872cf66d4SGreg Roach            'tree_id' => $this->id,
55913abd6f3SGreg Roach        ])->fetchOne();
560b78374c5SGreg Roach    }
561b78374c5SGreg Roach
562b78374c5SGreg Roach    /**
563a25f0a04SGreg Roach     * Delete all the genealogy data from a tree - in preparation for importing
564a25f0a04SGreg Roach     * new data. Optionally retain the media data, for when the user has been
565a25f0a04SGreg Roach     * editing their data offline using an application which deletes (or does not
566a25f0a04SGreg Roach     * support) media data.
567a25f0a04SGreg Roach     *
568a25f0a04SGreg Roach     * @param bool $keep_media
569b7e60af1SGreg Roach     *
570b7e60af1SGreg Roach     * @return void
571a25f0a04SGreg Roach     */
572b7e60af1SGreg Roach    public function deleteGenealogyData(bool $keep_media)
573c1010edaSGreg Roach    {
57472cf66d4SGreg Roach        Database::prepare("DELETE FROM `##gedcom_chunk` WHERE gedcom_id = ?")->execute([$this->id]);
57572cf66d4SGreg Roach        Database::prepare("DELETE FROM `##individuals`  WHERE i_file    = ?")->execute([$this->id]);
57672cf66d4SGreg Roach        Database::prepare("DELETE FROM `##families`     WHERE f_file    = ?")->execute([$this->id]);
57772cf66d4SGreg Roach        Database::prepare("DELETE FROM `##sources`      WHERE s_file    = ?")->execute([$this->id]);
57872cf66d4SGreg Roach        Database::prepare("DELETE FROM `##other`        WHERE o_file    = ?")->execute([$this->id]);
57972cf66d4SGreg Roach        Database::prepare("DELETE FROM `##places`       WHERE p_file    = ?")->execute([$this->id]);
58072cf66d4SGreg Roach        Database::prepare("DELETE FROM `##placelinks`   WHERE pl_file   = ?")->execute([$this->id]);
58172cf66d4SGreg Roach        Database::prepare("DELETE FROM `##name`         WHERE n_file    = ?")->execute([$this->id]);
58272cf66d4SGreg Roach        Database::prepare("DELETE FROM `##dates`        WHERE d_file    = ?")->execute([$this->id]);
58372cf66d4SGreg Roach        Database::prepare("DELETE FROM `##change`       WHERE gedcom_id = ?")->execute([$this->id]);
584a25f0a04SGreg Roach
585a25f0a04SGreg Roach        if ($keep_media) {
58672cf66d4SGreg Roach            Database::prepare("DELETE FROM `##link` WHERE l_file =? AND l_type<>'OBJE'")->execute([$this->id]);
587a25f0a04SGreg Roach        } else {
58872cf66d4SGreg Roach            Database::prepare("DELETE FROM `##link`  WHERE l_file =?")->execute([$this->id]);
58972cf66d4SGreg Roach            Database::prepare("DELETE FROM `##media` WHERE m_file =?")->execute([$this->id]);
59072cf66d4SGreg Roach            Database::prepare("DELETE FROM `##media_file` WHERE m_file =?")->execute([$this->id]);
591a25f0a04SGreg Roach        }
592a25f0a04SGreg Roach    }
593a25f0a04SGreg Roach
594a25f0a04SGreg Roach    /**
595a25f0a04SGreg Roach     * Delete everything relating to a tree
596b7e60af1SGreg Roach     *
597b7e60af1SGreg Roach     * @return void
598a25f0a04SGreg Roach     */
599c1010edaSGreg Roach    public function delete()
600c1010edaSGreg Roach    {
601a25f0a04SGreg Roach        // If this is the default tree, then unset it
602ef2fd529SGreg Roach        if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) {
603a25f0a04SGreg Roach            Site::setPreference('DEFAULT_GEDCOM', '');
604a25f0a04SGreg Roach        }
605a25f0a04SGreg Roach
606a25f0a04SGreg Roach        $this->deleteGenealogyData(false);
607a25f0a04SGreg Roach
60872cf66d4SGreg Roach        Database::prepare("DELETE `##block_setting` FROM `##block_setting` JOIN `##block` USING (block_id) WHERE gedcom_id=?")->execute([$this->id]);
60972cf66d4SGreg Roach        Database::prepare("DELETE FROM `##block`               WHERE gedcom_id = ?")->execute([$this->id]);
61072cf66d4SGreg Roach        Database::prepare("DELETE FROM `##user_gedcom_setting` WHERE gedcom_id = ?")->execute([$this->id]);
61172cf66d4SGreg Roach        Database::prepare("DELETE FROM `##gedcom_setting`      WHERE gedcom_id = ?")->execute([$this->id]);
61272cf66d4SGreg Roach        Database::prepare("DELETE FROM `##module_privacy`      WHERE gedcom_id = ?")->execute([$this->id]);
61372cf66d4SGreg Roach        Database::prepare("DELETE FROM `##hit_counter`         WHERE gedcom_id = ?")->execute([$this->id]);
61472cf66d4SGreg Roach        Database::prepare("DELETE FROM `##default_resn`        WHERE gedcom_id = ?")->execute([$this->id]);
61572cf66d4SGreg Roach        Database::prepare("DELETE FROM `##gedcom_chunk`        WHERE gedcom_id = ?")->execute([$this->id]);
61672cf66d4SGreg Roach        Database::prepare("DELETE FROM `##log`                 WHERE gedcom_id = ?")->execute([$this->id]);
61772cf66d4SGreg Roach        Database::prepare("DELETE FROM `##gedcom`              WHERE gedcom_id = ?")->execute([$this->id]);
618a25f0a04SGreg Roach
619a25f0a04SGreg Roach        // After updating the database, we need to fetch a new (sorted) copy
62075a9f908SGreg Roach        self::$trees = [];
621a25f0a04SGreg Roach    }
622a25f0a04SGreg Roach
623a25f0a04SGreg Roach    /**
624a25f0a04SGreg Roach     * Export the tree to a GEDCOM file
625a25f0a04SGreg Roach     *
6265792757eSGreg Roach     * @param resource $stream
627b7e60af1SGreg Roach     *
628b7e60af1SGreg Roach     * @return void
629a25f0a04SGreg Roach     */
630c1010edaSGreg Roach    public function exportGedcom($stream)
631c1010edaSGreg Roach    {
6325792757eSGreg Roach        $stmt = Database::prepare(
633e56ef01aSGreg Roach            "SELECT i_gedcom AS gedcom, i_id AS xref, 1 AS n FROM `##individuals` WHERE i_file = :tree_id_1" .
6345792757eSGreg Roach            " UNION ALL " .
635e56ef01aSGreg Roach            "SELECT f_gedcom AS gedcom, f_id AS xref, 2 AS n FROM `##families`    WHERE f_file = :tree_id_2" .
6365792757eSGreg Roach            " UNION ALL " .
637e56ef01aSGreg Roach            "SELECT s_gedcom AS gedcom, s_id AS xref, 3 AS n FROM `##sources`     WHERE s_file = :tree_id_3" .
6385792757eSGreg Roach            " UNION ALL " .
639e56ef01aSGreg Roach            "SELECT o_gedcom AS gedcom, o_id AS xref, 4 AS n FROM `##other`       WHERE o_file = :tree_id_4 AND o_type NOT IN ('HEAD', 'TRLR')" .
6405792757eSGreg Roach            " UNION ALL " .
641e56ef01aSGreg Roach            "SELECT m_gedcom AS gedcom, m_id AS xref, 5 AS n FROM `##media`       WHERE m_file = :tree_id_5" .
642e56ef01aSGreg Roach            " ORDER BY n, LENGTH(xref), xref"
64313abd6f3SGreg Roach        )->execute([
64472cf66d4SGreg Roach            'tree_id_1' => $this->id,
64572cf66d4SGreg Roach            'tree_id_2' => $this->id,
64672cf66d4SGreg Roach            'tree_id_3' => $this->id,
64772cf66d4SGreg Roach            'tree_id_4' => $this->id,
64872cf66d4SGreg Roach            'tree_id_5' => $this->id,
64913abd6f3SGreg Roach        ]);
650a25f0a04SGreg Roach
651a3d8780cSGreg Roach        $buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this, 'UTF-8'));
652a214e186SGreg Roach        while (($row = $stmt->fetch()) !== false) {
6533d7a8a4cSGreg Roach            $buffer .= FunctionsExport::reformatRecord($row->gedcom);
654a25f0a04SGreg Roach            if (strlen($buffer) > 65535) {
6555792757eSGreg Roach                fwrite($stream, $buffer);
656a25f0a04SGreg Roach                $buffer = '';
657a25f0a04SGreg Roach            }
658a25f0a04SGreg Roach        }
6590f471f91SGreg Roach        fwrite($stream, $buffer . '0 TRLR' . Gedcom::EOL);
660195d09d8SGreg Roach        $stmt->closeCursor();
661a25f0a04SGreg Roach    }
662a25f0a04SGreg Roach
663a25f0a04SGreg Roach    /**
664a25f0a04SGreg Roach     * Import data from a gedcom file into this tree.
665a25f0a04SGreg Roach     *
666a25f0a04SGreg Roach     * @param string $path     The full path to the (possibly temporary) file.
667a25f0a04SGreg Roach     * @param string $filename The preferred filename, for export/download.
668a25f0a04SGreg Roach     *
669b7e60af1SGreg Roach     * @return void
670b7e60af1SGreg Roach     * @throws Exception
671a25f0a04SGreg Roach     */
672771ae10aSGreg Roach    public function importGedcomFile(string $path, string $filename)
673c1010edaSGreg Roach    {
674a25f0a04SGreg Roach        // Read the file in blocks of roughly 64K. Ensure that each block
675a25f0a04SGreg Roach        // contains complete gedcom records. This will ensure we don’t split
676a25f0a04SGreg Roach        // multi-byte characters, as well as simplifying the code to import
677a25f0a04SGreg Roach        // each block.
678a25f0a04SGreg Roach
679a25f0a04SGreg Roach        $file_data = '';
680a25f0a04SGreg Roach        $fp        = fopen($path, 'rb');
681a25f0a04SGreg Roach
6822e897bf2SGreg Roach        if ($fp === false) {
6832e897bf2SGreg Roach            throw new Exception('Cannot write file: ' . $path);
6842e897bf2SGreg Roach        }
685a25f0a04SGreg Roach
686b7e60af1SGreg Roach        $this->deleteGenealogyData((bool) $this->getPreference('keep_media'));
687a25f0a04SGreg Roach        $this->setPreference('gedcom_filename', $filename);
688a25f0a04SGreg Roach        $this->setPreference('imported', '0');
689a25f0a04SGreg Roach
690a25f0a04SGreg Roach        while (!feof($fp)) {
691a25f0a04SGreg Roach            $file_data .= fread($fp, 65536);
692a25f0a04SGreg Roach            // There is no strrpos() function that searches for substrings :-(
693a25f0a04SGreg Roach            for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) {
694a25f0a04SGreg Roach                if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) {
695a25f0a04SGreg Roach                    // We’ve found the last record boundary in this chunk of data
696a25f0a04SGreg Roach                    break;
697a25f0a04SGreg Roach                }
698a25f0a04SGreg Roach            }
699a25f0a04SGreg Roach            if ($pos) {
700a25f0a04SGreg Roach                Database::prepare(
701a25f0a04SGreg Roach                    "INSERT INTO `##gedcom_chunk` (gedcom_id, chunk_data) VALUES (?, ?)"
702c1010edaSGreg Roach                )->execute([
70372cf66d4SGreg Roach                    $this->id,
704c1010edaSGreg Roach                    substr($file_data, 0, $pos),
705c1010edaSGreg Roach                ]);
706a25f0a04SGreg Roach                $file_data = substr($file_data, $pos);
707a25f0a04SGreg Roach            }
708a25f0a04SGreg Roach        }
709a25f0a04SGreg Roach        Database::prepare(
710a25f0a04SGreg Roach            "INSERT INTO `##gedcom_chunk` (gedcom_id, chunk_data) VALUES (?, ?)"
711c1010edaSGreg Roach        )->execute([
71272cf66d4SGreg Roach            $this->id,
713c1010edaSGreg Roach            $file_data,
714c1010edaSGreg Roach        ]);
715a25f0a04SGreg Roach
716a25f0a04SGreg Roach        fclose($fp);
717a25f0a04SGreg Roach    }
718304f20d5SGreg Roach
719304f20d5SGreg Roach    /**
720b90d8accSGreg Roach     * Generate a new XREF, unique across all family trees
721b90d8accSGreg Roach     *
722b90d8accSGreg Roach     * @return string
723b90d8accSGreg Roach     */
724771ae10aSGreg Roach    public function getNewXref(): string
725c1010edaSGreg Roach    {
726a214e186SGreg Roach        $prefix = 'X';
727b90d8accSGreg Roach
728971d66c8SGreg Roach        $increment = 1.0;
729b90d8accSGreg Roach        do {
730b90d8accSGreg Roach            // Use LAST_INSERT_ID(expr) to provide a transaction-safe sequence. See
731b90d8accSGreg Roach            // http://dev.mysql.com/doc/refman/5.6/en/information-functions.html#function_last-insert-id
732b90d8accSGreg Roach            $statement = Database::prepare(
733a214e186SGreg Roach                "UPDATE `##site_setting` SET setting_value = LAST_INSERT_ID(setting_value + :increment) WHERE setting_name = 'next_xref'"
734b90d8accSGreg Roach            );
73513abd6f3SGreg Roach            $statement->execute([
736971d66c8SGreg Roach                'increment' => (int) $increment,
73713abd6f3SGreg Roach            ]);
738b90d8accSGreg Roach
739b90d8accSGreg Roach            if ($statement->rowCount() === 0) {
740769d7d6eSGreg Roach                $num = '1';
741bbd8bd1bSGreg Roach                Site::setPreference('next_xref', $num);
742b90d8accSGreg Roach            } else {
743bbd8bd1bSGreg Roach                $num = (string) Database::prepare("SELECT LAST_INSERT_ID()")->fetchOne();
744b90d8accSGreg Roach            }
745b90d8accSGreg Roach
746a214e186SGreg Roach            $xref = $prefix . $num;
747a214e186SGreg Roach
748b90d8accSGreg Roach            // Records may already exist with this sequence number.
749b90d8accSGreg Roach            $already_used = Database::prepare(
750a214e186SGreg Roach                "SELECT" .
751a214e186SGreg Roach                " EXISTS (SELECT 1 FROM `##individuals` WHERE i_id = :i_id) OR" .
752a214e186SGreg Roach                " EXISTS (SELECT 1 FROM `##families` WHERE f_id = :f_id) OR" .
753a214e186SGreg Roach                " EXISTS (SELECT 1 FROM `##sources` WHERE s_id = :s_id) OR" .
754a214e186SGreg Roach                " EXISTS (SELECT 1 FROM `##media` WHERE m_id = :m_id) OR" .
755a214e186SGreg Roach                " EXISTS (SELECT 1 FROM `##other` WHERE o_id = :o_id) OR" .
756a214e186SGreg Roach                " EXISTS (SELECT 1 FROM `##change` WHERE xref = :xref)"
75713abd6f3SGreg Roach            )->execute([
758a214e186SGreg Roach                'i_id' => $xref,
759a214e186SGreg Roach                'f_id' => $xref,
760a214e186SGreg Roach                's_id' => $xref,
761a214e186SGreg Roach                'm_id' => $xref,
762a214e186SGreg Roach                'o_id' => $xref,
763a214e186SGreg Roach                'xref' => $xref,
76413abd6f3SGreg Roach            ])->fetchOne();
765971d66c8SGreg Roach
766971d66c8SGreg Roach            // This exponential increment allows us to scan over large blocks of
767971d66c8SGreg Roach            // existing data in a reasonable time.
768971d66c8SGreg Roach            $increment *= 1.01;
769a214e186SGreg Roach        } while ($already_used !== '0');
770b90d8accSGreg Roach
771a214e186SGreg Roach        return $xref;
772b90d8accSGreg Roach    }
773b90d8accSGreg Roach
774b90d8accSGreg Roach    /**
775304f20d5SGreg Roach     * Create a new record from GEDCOM data.
776304f20d5SGreg Roach     *
777304f20d5SGreg Roach     * @param string $gedcom
778304f20d5SGreg Roach     *
77915d603e7SGreg Roach     * @return GedcomRecord|Individual|Family|Note|Source|Repository|Media
780afb591d7SGreg Roach     * @throws InvalidArgumentException
781304f20d5SGreg Roach     */
782771ae10aSGreg Roach    public function createRecord(string $gedcom): GedcomRecord
783c1010edaSGreg Roach    {
784afb591d7SGreg Roach        if (substr_compare($gedcom, '0 @@', 0, 4) !== 0) {
785afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@');
786304f20d5SGreg Roach        }
787304f20d5SGreg Roach
788a214e186SGreg Roach        $xref   = $this->getNewXref();
789afb591d7SGreg Roach        $gedcom = '0 @' . $xref . '@' . substr($gedcom, 4);
790304f20d5SGreg Roach
791afb591d7SGreg Roach        // Create a change record
792304f20d5SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->getUserName();
793304f20d5SGreg Roach
794304f20d5SGreg Roach        // Create a pending change
795304f20d5SGreg Roach        Database::prepare(
796304f20d5SGreg Roach            "INSERT INTO `##change` (gedcom_id, xref, old_gedcom, new_gedcom, user_id) VALUES (?, ?, '', ?, ?)"
79713abd6f3SGreg Roach        )->execute([
79872cf66d4SGreg Roach            $this->id,
799304f20d5SGreg Roach            $xref,
800304f20d5SGreg Roach            $gedcom,
801cbc1590aSGreg Roach            Auth::id(),
80213abd6f3SGreg Roach        ]);
803304f20d5SGreg Roach
804afb591d7SGreg Roach        // Accept this pending change
805afb591d7SGreg Roach        if (Auth::user()->getPreference('auto_accept')) {
806afb591d7SGreg Roach            FunctionsImport::acceptAllChanges($xref, $this);
807afb591d7SGreg Roach
808afb591d7SGreg Roach            return new GedcomRecord($xref, $gedcom, null, $this);
809afb591d7SGreg Roach        }
810afb591d7SGreg Roach
811313e72b3SGreg Roach        return GedcomRecord::getInstance($xref, $this, $gedcom);
812afb591d7SGreg Roach    }
813afb591d7SGreg Roach
814afb591d7SGreg Roach    /**
815afb591d7SGreg Roach     * Create a new family from GEDCOM data.
816afb591d7SGreg Roach     *
817afb591d7SGreg Roach     * @param string $gedcom
818afb591d7SGreg Roach     *
819afb591d7SGreg Roach     * @return Family
820afb591d7SGreg Roach     * @throws InvalidArgumentException
821afb591d7SGreg Roach     */
822afb591d7SGreg Roach    public function createFamily(string $gedcom): GedcomRecord
823afb591d7SGreg Roach    {
824afb591d7SGreg Roach        if (substr_compare($gedcom, '0 @@ FAM', 0, 8) !== 0) {
825afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM');
826afb591d7SGreg Roach        }
827afb591d7SGreg Roach
828afb591d7SGreg Roach        $xref   = $this->getNewXref();
829afb591d7SGreg Roach        $gedcom = '0 @' . $xref . '@' . substr($gedcom, 4);
830afb591d7SGreg Roach
831afb591d7SGreg Roach        // Create a change record
832afb591d7SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->getUserName();
833afb591d7SGreg Roach
834afb591d7SGreg Roach        // Create a pending change
835afb591d7SGreg Roach        Database::prepare(
836afb591d7SGreg Roach            "INSERT INTO `##change` (gedcom_id, xref, old_gedcom, new_gedcom, user_id) VALUES (?, ?, '', ?, ?)"
837afb591d7SGreg Roach        )->execute([
83872cf66d4SGreg Roach            $this->id,
839afb591d7SGreg Roach            $xref,
840afb591d7SGreg Roach            $gedcom,
841afb591d7SGreg Roach            Auth::id(),
842afb591d7SGreg Roach        ]);
843304f20d5SGreg Roach
844304f20d5SGreg Roach        // Accept this pending change
845304f20d5SGreg Roach        if (Auth::user()->getPreference('auto_accept')) {
846cc5684fdSGreg Roach            FunctionsImport::acceptAllChanges($xref, $this);
847afb591d7SGreg Roach
848afb591d7SGreg Roach            return new Family($xref, $gedcom, null, $this);
849304f20d5SGreg Roach        }
850afb591d7SGreg Roach
851afb591d7SGreg Roach        return new Family($xref, '', $gedcom, $this);
852afb591d7SGreg Roach    }
853afb591d7SGreg Roach
854afb591d7SGreg Roach    /**
855afb591d7SGreg Roach     * Create a new individual from GEDCOM data.
856afb591d7SGreg Roach     *
857afb591d7SGreg Roach     * @param string $gedcom
858afb591d7SGreg Roach     *
859afb591d7SGreg Roach     * @return Individual
860afb591d7SGreg Roach     * @throws InvalidArgumentException
861afb591d7SGreg Roach     */
862afb591d7SGreg Roach    public function createIndividual(string $gedcom): GedcomRecord
863afb591d7SGreg Roach    {
864afb591d7SGreg Roach        if (substr_compare($gedcom, '0 @@ INDI', 0, 9) !== 0) {
865afb591d7SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI');
866afb591d7SGreg Roach        }
867afb591d7SGreg Roach
868afb591d7SGreg Roach        $xref   = $this->getNewXref();
869afb591d7SGreg Roach        $gedcom = '0 @' . $xref . '@' . substr($gedcom, 4);
870afb591d7SGreg Roach
871afb591d7SGreg Roach        // Create a change record
872afb591d7SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->getUserName();
873afb591d7SGreg Roach
874afb591d7SGreg Roach        // Create a pending change
875afb591d7SGreg Roach        Database::prepare(
876afb591d7SGreg Roach            "INSERT INTO `##change` (gedcom_id, xref, old_gedcom, new_gedcom, user_id) VALUES (?, ?, '', ?, ?)"
877afb591d7SGreg Roach        )->execute([
87872cf66d4SGreg Roach            $this->id,
879afb591d7SGreg Roach            $xref,
880afb591d7SGreg Roach            $gedcom,
881afb591d7SGreg Roach            Auth::id(),
882afb591d7SGreg Roach        ]);
883afb591d7SGreg Roach
884afb591d7SGreg Roach        // Accept this pending change
885afb591d7SGreg Roach        if (Auth::user()->getPreference('auto_accept')) {
886afb591d7SGreg Roach            FunctionsImport::acceptAllChanges($xref, $this);
887afb591d7SGreg Roach
888afb591d7SGreg Roach            return new Individual($xref, $gedcom, null, $this);
889afb591d7SGreg Roach        }
890afb591d7SGreg Roach
891afb591d7SGreg Roach        return new Individual($xref, '', $gedcom, $this);
892304f20d5SGreg Roach    }
8938586983fSGreg Roach
8948586983fSGreg Roach    /**
89520b58d20SGreg Roach     * Create a new media object from GEDCOM data.
89620b58d20SGreg Roach     *
89720b58d20SGreg Roach     * @param string $gedcom
89820b58d20SGreg Roach     *
89920b58d20SGreg Roach     * @return Media
90020b58d20SGreg Roach     * @throws InvalidArgumentException
90120b58d20SGreg Roach     */
90220b58d20SGreg Roach    public function createMediaObject(string $gedcom): Media
90320b58d20SGreg Roach    {
90420b58d20SGreg Roach        if (substr_compare($gedcom, '0 @@ OBJE', 0, 9) !== 0) {
90520b58d20SGreg Roach            throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE');
90620b58d20SGreg Roach        }
90720b58d20SGreg Roach
90820b58d20SGreg Roach        $xref   = $this->getNewXref();
90920b58d20SGreg Roach        $gedcom = '0 @' . $xref . '@' . substr($gedcom, 4);
91020b58d20SGreg Roach
91120b58d20SGreg Roach        // Create a change record
91220b58d20SGreg Roach        $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->getUserName();
91320b58d20SGreg Roach
91420b58d20SGreg Roach        // Create a pending change
91520b58d20SGreg Roach        Database::prepare(
91620b58d20SGreg Roach            "INSERT INTO `##change` (gedcom_id, xref, old_gedcom, new_gedcom, user_id) VALUES (?, ?, '', ?, ?)"
91720b58d20SGreg Roach        )->execute([
91820b58d20SGreg Roach            $this->id,
91920b58d20SGreg Roach            $xref,
92020b58d20SGreg Roach            $gedcom,
92120b58d20SGreg Roach            Auth::id(),
92220b58d20SGreg Roach        ]);
92320b58d20SGreg Roach
92420b58d20SGreg Roach        // Accept this pending change
92520b58d20SGreg Roach        if (Auth::user()->getPreference('auto_accept')) {
92620b58d20SGreg Roach            FunctionsImport::acceptAllChanges($xref, $this);
92720b58d20SGreg Roach
92820b58d20SGreg Roach            return new Media($xref, $gedcom, null, $this);
92920b58d20SGreg Roach        }
93020b58d20SGreg Roach
93120b58d20SGreg Roach        return new Media($xref, '', $gedcom, $this);
93220b58d20SGreg Roach    }
93320b58d20SGreg Roach
93420b58d20SGreg Roach    /**
9358586983fSGreg Roach     * What is the most significant individual in this tree.
9368586983fSGreg Roach     *
9378586983fSGreg Roach     * @param User $user
9388586983fSGreg Roach     *
9398586983fSGreg Roach     * @return Individual
9408586983fSGreg Roach     */
941c1010edaSGreg Roach    public function significantIndividual(User $user): Individual
942c1010edaSGreg Roach    {
9438586983fSGreg Roach        static $individual; // Only query the DB once.
9448586983fSGreg Roach
9457015ba1fSGreg Roach        if (!$individual && $this->getUserPreference($user, 'rootid') !== '') {
9468586983fSGreg Roach            $individual = Individual::getInstance($this->getUserPreference($user, 'rootid'), $this);
9478586983fSGreg Roach        }
9487015ba1fSGreg Roach        if (!$individual && $this->getUserPreference($user, 'gedcomid') !== '') {
9498586983fSGreg Roach            $individual = Individual::getInstance($this->getUserPreference($user, 'gedcomid'), $this);
9508586983fSGreg Roach        }
9518586983fSGreg Roach        if (!$individual) {
9528586983fSGreg Roach            $individual = Individual::getInstance($this->getPreference('PEDIGREE_ROOT_ID'), $this);
9538586983fSGreg Roach        }
9548586983fSGreg Roach        if (!$individual) {
955769d7d6eSGreg Roach            $xref = (string) Database::prepare(
9565fe1add5SGreg Roach                "SELECT MIN(i_id) FROM `##individuals` WHERE i_file = :tree_id"
9575fe1add5SGreg Roach            )->execute([
95872cf66d4SGreg Roach                'tree_id' => $this->id(),
959769d7d6eSGreg Roach            ])->fetchOne();
960769d7d6eSGreg Roach
961769d7d6eSGreg Roach            $individual = Individual::getInstance($xref, $this);
9625fe1add5SGreg Roach        }
9635fe1add5SGreg Roach        if (!$individual) {
9645fe1add5SGreg Roach            // always return a record
9655fe1add5SGreg Roach            $individual = new Individual('I', '0 @I@ INDI', null, $this);
9665fe1add5SGreg Roach        }
9675fe1add5SGreg Roach
9685fe1add5SGreg Roach        return $individual;
9695fe1add5SGreg Roach    }
9705fe1add5SGreg Roach
9715fe1add5SGreg Roach    /**
9725fe1add5SGreg Roach     * Get significant information from this page, to allow other pages such as
9735fe1add5SGreg Roach     * charts and reports to initialise with the same records
9745fe1add5SGreg Roach     *
9755fe1add5SGreg Roach     * @return Individual
9765fe1add5SGreg Roach     */
977771ae10aSGreg Roach    public function getSignificantIndividual(): Individual
978c1010edaSGreg Roach    {
9795fe1add5SGreg Roach        static $individual; // Only query the DB once.
9805fe1add5SGreg Roach
9817015ba1fSGreg Roach        if (!$individual && $this->getUserPreference(Auth::user(), 'rootid') !== '') {
9825fe1add5SGreg Roach            $individual = Individual::getInstance($this->getUserPreference(Auth::user(), 'rootid'), $this);
9835fe1add5SGreg Roach        }
9847015ba1fSGreg Roach        if (!$individual && $this->getUserPreference(Auth::user(), 'gedcomid') !== '') {
9855fe1add5SGreg Roach            $individual = Individual::getInstance($this->getUserPreference(Auth::user(), 'gedcomid'), $this);
9865fe1add5SGreg Roach        }
9875fe1add5SGreg Roach        if (!$individual) {
9885fe1add5SGreg Roach            $individual = Individual::getInstance($this->getPreference('PEDIGREE_ROOT_ID'), $this);
9895fe1add5SGreg Roach        }
9905fe1add5SGreg Roach        if (!$individual) {
991769d7d6eSGreg Roach            $xref = (string) Database::prepare(
992769d7d6eSGreg Roach                "SELECT MIN(i_id) FROM `##individuals` WHERE i_file = :tree_id"
993769d7d6eSGreg Roach            )->execute([
99472cf66d4SGreg Roach                'tree_id' => $this->id(),
995769d7d6eSGreg Roach            ])->fetchOne();
996769d7d6eSGreg Roach
997769d7d6eSGreg Roach            $individual = Individual::getInstance($xref, $this);
9988586983fSGreg Roach        }
9998586983fSGreg Roach        if (!$individual) {
10008586983fSGreg Roach            // always return a record
10018586983fSGreg Roach            $individual = new Individual('I', '0 @I@ INDI', null, $this);
10028586983fSGreg Roach        }
10038586983fSGreg Roach
10048586983fSGreg Roach        return $individual;
10058586983fSGreg Roach    }
1006a25f0a04SGreg Roach}
1007