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