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