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; 2622e73debSGreg Roachuse Fisharebest\Webtrees\Services\PendingChangesService; 2701461f86SGreg Roachuse Illuminate\Database\Capsule\Manager as DB; 28a69f5655SGreg Roachuse Illuminate\Database\Query\Expression; 2994026f20SGreg Roachuse Illuminate\Support\Collection; 30bec87e94SGreg Roachuse Illuminate\Support\Str; 31afb591d7SGreg Roachuse InvalidArgumentException; 321df7ae39SGreg Roachuse League\Flysystem\Filesystem; 331df7ae39SGreg Roachuse League\Flysystem\FilesystemInterface; 346ccdf4f0SGreg Roachuse Psr\Http\Message\StreamInterface; 358b67c11aSGreg Roachuse stdClass; 36a25f0a04SGreg Roach 371e653452SGreg Roachuse function app; 3853432476SGreg Roachuse function date; 3953432476SGreg Roachuse function strtoupper; 401e653452SGreg Roach 41a25f0a04SGreg Roach/** 4276692c8bSGreg Roach * Provide an interface to the wt_gedcom table. 43a25f0a04SGreg Roach */ 44c1010edaSGreg Roachclass Tree 45c1010edaSGreg Roach{ 46061b43d7SGreg Roach private const RESN_PRIVACY = [ 47061b43d7SGreg Roach 'none' => Auth::PRIV_PRIVATE, 48061b43d7SGreg Roach 'privacy' => Auth::PRIV_USER, 49061b43d7SGreg Roach 'confidential' => Auth::PRIV_NONE, 50061b43d7SGreg Roach 'hidden' => Auth::PRIV_HIDE, 51061b43d7SGreg Roach ]; 523df1e584SGreg Roach 536ccdf4f0SGreg Roach /** @var int The tree's ID number */ 546ccdf4f0SGreg Roach private $id; 553df1e584SGreg Roach 566ccdf4f0SGreg Roach /** @var string The tree's name */ 576ccdf4f0SGreg Roach private $name; 583df1e584SGreg Roach 596ccdf4f0SGreg Roach /** @var string The tree's title */ 606ccdf4f0SGreg Roach private $title; 613df1e584SGreg Roach 626ccdf4f0SGreg Roach /** @var int[] Default access rules for facts in this tree */ 636ccdf4f0SGreg Roach private $fact_privacy; 643df1e584SGreg Roach 656ccdf4f0SGreg Roach /** @var int[] Default access rules for individuals in this tree */ 666ccdf4f0SGreg Roach private $individual_privacy; 673df1e584SGreg Roach 686ccdf4f0SGreg Roach /** @var integer[][] Default access rules for individual facts in this tree */ 696ccdf4f0SGreg Roach private $individual_fact_privacy; 703df1e584SGreg Roach 716ccdf4f0SGreg Roach /** @var string[] Cached copy of the wt_gedcom_setting table. */ 726ccdf4f0SGreg Roach private $preferences = []; 733df1e584SGreg Roach 746ccdf4f0SGreg Roach /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */ 756ccdf4f0SGreg Roach private $user_preferences = []; 76061b43d7SGreg Roach 77a25f0a04SGreg Roach /** 783df1e584SGreg Roach * Create a tree object. 79a25f0a04SGreg Roach * 8072cf66d4SGreg Roach * @param int $id 81aa6f03bbSGreg Roach * @param string $name 82cc13d6d8SGreg Roach * @param string $title 83a25f0a04SGreg Roach */ 845afbc57aSGreg Roach public function __construct(int $id, string $name, string $title) 85c1010edaSGreg Roach { 8672cf66d4SGreg Roach $this->id = $id; 87aa6f03bbSGreg Roach $this->name = $name; 88cc13d6d8SGreg Roach $this->title = $title; 8913abd6f3SGreg Roach $this->fact_privacy = []; 9013abd6f3SGreg Roach $this->individual_privacy = []; 9113abd6f3SGreg Roach $this->individual_fact_privacy = []; 92518bbdc1SGreg Roach 93518bbdc1SGreg Roach // Load the privacy settings for this tree 94061b43d7SGreg Roach $rows = DB::table('default_resn') 95061b43d7SGreg Roach ->where('gedcom_id', '=', $this->id) 96061b43d7SGreg Roach ->get(); 97518bbdc1SGreg Roach 98518bbdc1SGreg Roach foreach ($rows as $row) { 99061b43d7SGreg Roach // Convert GEDCOM privacy restriction to a webtrees access level. 100061b43d7SGreg Roach $row->resn = self::RESN_PRIVACY[$row->resn]; 101061b43d7SGreg Roach 102518bbdc1SGreg Roach if ($row->xref !== null) { 103518bbdc1SGreg Roach if ($row->tag_type !== null) { 104b262b3d3SGreg Roach $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn; 105518bbdc1SGreg Roach } else { 106b262b3d3SGreg Roach $this->individual_privacy[$row->xref] = $row->resn; 107518bbdc1SGreg Roach } 108518bbdc1SGreg Roach } else { 109b262b3d3SGreg Roach $this->fact_privacy[$row->tag_type] = $row->resn; 110518bbdc1SGreg Roach } 111518bbdc1SGreg Roach } 112a25f0a04SGreg Roach } 113a25f0a04SGreg Roach 114a25f0a04SGreg Roach /** 1155afbc57aSGreg Roach * A closure which will create a record from a database row. 1165afbc57aSGreg Roach * 1175afbc57aSGreg Roach * @return Closure 1185afbc57aSGreg Roach */ 1195afbc57aSGreg Roach public static function rowMapper(): Closure 1205afbc57aSGreg Roach { 1215afbc57aSGreg Roach return static function (stdClass $row): Tree { 1225afbc57aSGreg Roach return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title); 1235afbc57aSGreg Roach }; 1245afbc57aSGreg Roach } 1255afbc57aSGreg Roach 1265afbc57aSGreg Roach /** 1276ccdf4f0SGreg Roach * Set the tree’s configuration settings. 1286ccdf4f0SGreg Roach * 1296ccdf4f0SGreg Roach * @param string $setting_name 1306ccdf4f0SGreg Roach * @param string $setting_value 1316ccdf4f0SGreg Roach * 1326ccdf4f0SGreg Roach * @return $this 1336ccdf4f0SGreg Roach */ 1346ccdf4f0SGreg Roach public function setPreference(string $setting_name, string $setting_value): Tree 1356ccdf4f0SGreg Roach { 1366ccdf4f0SGreg Roach if ($setting_value !== $this->getPreference($setting_name)) { 1376ccdf4f0SGreg Roach DB::table('gedcom_setting')->updateOrInsert([ 1386ccdf4f0SGreg Roach 'gedcom_id' => $this->id, 1396ccdf4f0SGreg Roach 'setting_name' => $setting_name, 1406ccdf4f0SGreg Roach ], [ 1416ccdf4f0SGreg Roach 'setting_value' => $setting_value, 1426ccdf4f0SGreg Roach ]); 1436ccdf4f0SGreg Roach 1446ccdf4f0SGreg Roach $this->preferences[$setting_name] = $setting_value; 1456ccdf4f0SGreg Roach 1466ccdf4f0SGreg Roach Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this); 1476ccdf4f0SGreg Roach } 1486ccdf4f0SGreg Roach 1496ccdf4f0SGreg Roach return $this; 1506ccdf4f0SGreg Roach } 1516ccdf4f0SGreg Roach 1526ccdf4f0SGreg Roach /** 1536ccdf4f0SGreg Roach * Get the tree’s configuration settings. 1546ccdf4f0SGreg Roach * 1556ccdf4f0SGreg Roach * @param string $setting_name 1566ccdf4f0SGreg Roach * @param string $default 1576ccdf4f0SGreg Roach * 1586ccdf4f0SGreg Roach * @return string 1596ccdf4f0SGreg Roach */ 1606ccdf4f0SGreg Roach public function getPreference(string $setting_name, string $default = ''): string 1616ccdf4f0SGreg Roach { 16254c1ab5eSGreg Roach if ($this->preferences === []) { 1636ccdf4f0SGreg Roach $this->preferences = DB::table('gedcom_setting') 1646ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 1656ccdf4f0SGreg Roach ->pluck('setting_value', 'setting_name') 1666ccdf4f0SGreg Roach ->all(); 1676ccdf4f0SGreg Roach } 1686ccdf4f0SGreg Roach 1696ccdf4f0SGreg Roach return $this->preferences[$setting_name] ?? $default; 1706ccdf4f0SGreg Roach } 1716ccdf4f0SGreg Roach 1726ccdf4f0SGreg Roach /** 1736ccdf4f0SGreg Roach * The name of this tree 1746ccdf4f0SGreg Roach * 1756ccdf4f0SGreg Roach * @return string 1766ccdf4f0SGreg Roach */ 1776ccdf4f0SGreg Roach public function name(): string 1786ccdf4f0SGreg Roach { 1796ccdf4f0SGreg Roach return $this->name; 1806ccdf4f0SGreg Roach } 1816ccdf4f0SGreg Roach 1826ccdf4f0SGreg Roach /** 1836ccdf4f0SGreg Roach * The title of this tree 1846ccdf4f0SGreg Roach * 1856ccdf4f0SGreg Roach * @return string 1866ccdf4f0SGreg Roach */ 1876ccdf4f0SGreg Roach public function title(): string 1886ccdf4f0SGreg Roach { 1896ccdf4f0SGreg Roach return $this->title; 1906ccdf4f0SGreg Roach } 1916ccdf4f0SGreg Roach 1926ccdf4f0SGreg Roach /** 1936ccdf4f0SGreg Roach * The fact-level privacy for this tree. 1946ccdf4f0SGreg Roach * 1956ccdf4f0SGreg Roach * @return int[] 1966ccdf4f0SGreg Roach */ 1976ccdf4f0SGreg Roach public function getFactPrivacy(): array 1986ccdf4f0SGreg Roach { 1996ccdf4f0SGreg Roach return $this->fact_privacy; 2006ccdf4f0SGreg Roach } 2016ccdf4f0SGreg Roach 2026ccdf4f0SGreg Roach /** 2036ccdf4f0SGreg Roach * The individual-level privacy for this tree. 2046ccdf4f0SGreg Roach * 2056ccdf4f0SGreg Roach * @return int[] 2066ccdf4f0SGreg Roach */ 2076ccdf4f0SGreg Roach public function getIndividualPrivacy(): array 2086ccdf4f0SGreg Roach { 2096ccdf4f0SGreg Roach return $this->individual_privacy; 2106ccdf4f0SGreg Roach } 2116ccdf4f0SGreg Roach 2126ccdf4f0SGreg Roach /** 2136ccdf4f0SGreg Roach * The individual-fact-level privacy for this tree. 2146ccdf4f0SGreg Roach * 2156ccdf4f0SGreg Roach * @return int[][] 2166ccdf4f0SGreg Roach */ 2176ccdf4f0SGreg Roach public function getIndividualFactPrivacy(): array 2186ccdf4f0SGreg Roach { 2196ccdf4f0SGreg Roach return $this->individual_fact_privacy; 2206ccdf4f0SGreg Roach } 2216ccdf4f0SGreg Roach 2226ccdf4f0SGreg Roach /** 2236ccdf4f0SGreg Roach * Set the tree’s user-configuration settings. 2246ccdf4f0SGreg Roach * 2256ccdf4f0SGreg Roach * @param UserInterface $user 2266ccdf4f0SGreg Roach * @param string $setting_name 2276ccdf4f0SGreg Roach * @param string $setting_value 2286ccdf4f0SGreg Roach * 2296ccdf4f0SGreg Roach * @return $this 2306ccdf4f0SGreg Roach */ 2316ccdf4f0SGreg Roach public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree 2326ccdf4f0SGreg Roach { 2336ccdf4f0SGreg Roach if ($this->getUserPreference($user, $setting_name) !== $setting_value) { 2346ccdf4f0SGreg Roach // Update the database 2356ccdf4f0SGreg Roach DB::table('user_gedcom_setting')->updateOrInsert([ 2366ccdf4f0SGreg Roach 'gedcom_id' => $this->id(), 2376ccdf4f0SGreg Roach 'user_id' => $user->id(), 2386ccdf4f0SGreg Roach 'setting_name' => $setting_name, 2396ccdf4f0SGreg Roach ], [ 2406ccdf4f0SGreg Roach 'setting_value' => $setting_value, 2416ccdf4f0SGreg Roach ]); 2426ccdf4f0SGreg Roach 2436ccdf4f0SGreg Roach // Update the cache 2446ccdf4f0SGreg Roach $this->user_preferences[$user->id()][$setting_name] = $setting_value; 2456ccdf4f0SGreg Roach // Audit log of changes 2466ccdf4f0SGreg Roach Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this); 2476ccdf4f0SGreg Roach } 2486ccdf4f0SGreg Roach 2496ccdf4f0SGreg Roach return $this; 2506ccdf4f0SGreg Roach } 2516ccdf4f0SGreg Roach 2526ccdf4f0SGreg Roach /** 2536ccdf4f0SGreg Roach * Get the tree’s user-configuration settings. 2546ccdf4f0SGreg Roach * 2556ccdf4f0SGreg Roach * @param UserInterface $user 2566ccdf4f0SGreg Roach * @param string $setting_name 2576ccdf4f0SGreg Roach * @param string $default 2586ccdf4f0SGreg Roach * 2596ccdf4f0SGreg Roach * @return string 2606ccdf4f0SGreg Roach */ 2616ccdf4f0SGreg Roach public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string 2626ccdf4f0SGreg Roach { 2636ccdf4f0SGreg Roach // There are lots of settings, and we need to fetch lots of them on every page 2646ccdf4f0SGreg Roach // so it is quicker to fetch them all in one go. 2656ccdf4f0SGreg Roach if (!array_key_exists($user->id(), $this->user_preferences)) { 2666ccdf4f0SGreg Roach $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting') 2676ccdf4f0SGreg Roach ->where('user_id', '=', $user->id()) 2686ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 2696ccdf4f0SGreg Roach ->pluck('setting_value', 'setting_name') 2706ccdf4f0SGreg Roach ->all(); 2716ccdf4f0SGreg Roach } 2726ccdf4f0SGreg Roach 2736ccdf4f0SGreg Roach return $this->user_preferences[$user->id()][$setting_name] ?? $default; 2746ccdf4f0SGreg Roach } 2756ccdf4f0SGreg Roach 2766ccdf4f0SGreg Roach /** 2776ccdf4f0SGreg Roach * The ID of this tree 2786ccdf4f0SGreg Roach * 2796ccdf4f0SGreg Roach * @return int 2806ccdf4f0SGreg Roach */ 2816ccdf4f0SGreg Roach public function id(): int 2826ccdf4f0SGreg Roach { 2836ccdf4f0SGreg Roach return $this->id; 2846ccdf4f0SGreg Roach } 2856ccdf4f0SGreg Roach 2866ccdf4f0SGreg Roach /** 2876ccdf4f0SGreg Roach * Can a user accept changes for this tree? 2886ccdf4f0SGreg Roach * 2896ccdf4f0SGreg Roach * @param UserInterface $user 2906ccdf4f0SGreg Roach * 2916ccdf4f0SGreg Roach * @return bool 2926ccdf4f0SGreg Roach */ 2936ccdf4f0SGreg Roach public function canAcceptChanges(UserInterface $user): bool 2946ccdf4f0SGreg Roach { 2956ccdf4f0SGreg Roach return Auth::isModerator($this, $user); 2966ccdf4f0SGreg Roach } 2976ccdf4f0SGreg Roach 2986ccdf4f0SGreg Roach /** 299b78374c5SGreg Roach * Are there any pending edits for this tree, than need reviewing by a moderator. 300b78374c5SGreg Roach * 301b78374c5SGreg Roach * @return bool 302b78374c5SGreg Roach */ 303771ae10aSGreg Roach public function hasPendingEdit(): bool 304c1010edaSGreg Roach { 30515a3f100SGreg Roach return DB::table('change') 30615a3f100SGreg Roach ->where('gedcom_id', '=', $this->id) 30715a3f100SGreg Roach ->where('status', '=', 'pending') 30815a3f100SGreg Roach ->exists(); 309b78374c5SGreg Roach } 310b78374c5SGreg Roach 311b78374c5SGreg Roach /** 3126ccdf4f0SGreg Roach * Delete everything relating to a tree 3136ccdf4f0SGreg Roach * 3146ccdf4f0SGreg Roach * @return void 3156ccdf4f0SGreg Roach */ 3166ccdf4f0SGreg Roach public function delete(): void 3176ccdf4f0SGreg Roach { 3186ccdf4f0SGreg Roach // If this is the default tree, then unset it 3196ccdf4f0SGreg Roach if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) { 3206ccdf4f0SGreg Roach Site::setPreference('DEFAULT_GEDCOM', ''); 3216ccdf4f0SGreg Roach } 3226ccdf4f0SGreg Roach 3236ccdf4f0SGreg Roach $this->deleteGenealogyData(false); 3246ccdf4f0SGreg Roach 3256ccdf4f0SGreg Roach DB::table('block_setting') 3266ccdf4f0SGreg Roach ->join('block', 'block.block_id', '=', 'block_setting.block_id') 3276ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 3286ccdf4f0SGreg Roach ->delete(); 3296ccdf4f0SGreg Roach DB::table('block')->where('gedcom_id', '=', $this->id)->delete(); 3306ccdf4f0SGreg Roach DB::table('user_gedcom_setting')->where('gedcom_id', '=', $this->id)->delete(); 3316ccdf4f0SGreg Roach DB::table('gedcom_setting')->where('gedcom_id', '=', $this->id)->delete(); 3326ccdf4f0SGreg Roach DB::table('module_privacy')->where('gedcom_id', '=', $this->id)->delete(); 3336ccdf4f0SGreg Roach DB::table('hit_counter')->where('gedcom_id', '=', $this->id)->delete(); 3346ccdf4f0SGreg Roach DB::table('default_resn')->where('gedcom_id', '=', $this->id)->delete(); 3356ccdf4f0SGreg Roach DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete(); 3366ccdf4f0SGreg Roach DB::table('log')->where('gedcom_id', '=', $this->id)->delete(); 3376ccdf4f0SGreg Roach DB::table('gedcom')->where('gedcom_id', '=', $this->id)->delete(); 3386ccdf4f0SGreg Roach } 3396ccdf4f0SGreg Roach 3406ccdf4f0SGreg Roach /** 341a25f0a04SGreg Roach * Delete all the genealogy data from a tree - in preparation for importing 342a25f0a04SGreg Roach * new data. Optionally retain the media data, for when the user has been 343a25f0a04SGreg Roach * editing their data offline using an application which deletes (or does not 344a25f0a04SGreg Roach * support) media data. 345a25f0a04SGreg Roach * 346a25f0a04SGreg Roach * @param bool $keep_media 347b7e60af1SGreg Roach * 348b7e60af1SGreg Roach * @return void 349a25f0a04SGreg Roach */ 350e364afe4SGreg Roach public function deleteGenealogyData(bool $keep_media): void 351c1010edaSGreg Roach { 3521ad2dde6SGreg Roach DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete(); 3531ad2dde6SGreg Roach DB::table('individuals')->where('i_file', '=', $this->id)->delete(); 3541ad2dde6SGreg Roach DB::table('families')->where('f_file', '=', $this->id)->delete(); 3551ad2dde6SGreg Roach DB::table('sources')->where('s_file', '=', $this->id)->delete(); 3561ad2dde6SGreg Roach DB::table('other')->where('o_file', '=', $this->id)->delete(); 3571ad2dde6SGreg Roach DB::table('places')->where('p_file', '=', $this->id)->delete(); 3581ad2dde6SGreg Roach DB::table('placelinks')->where('pl_file', '=', $this->id)->delete(); 3591ad2dde6SGreg Roach DB::table('name')->where('n_file', '=', $this->id)->delete(); 3601ad2dde6SGreg Roach DB::table('dates')->where('d_file', '=', $this->id)->delete(); 3611ad2dde6SGreg Roach DB::table('change')->where('gedcom_id', '=', $this->id)->delete(); 362a25f0a04SGreg Roach 363a25f0a04SGreg Roach if ($keep_media) { 3641ad2dde6SGreg Roach DB::table('link')->where('l_file', '=', $this->id) 3651ad2dde6SGreg Roach ->where('l_type', '<>', 'OBJE') 3661ad2dde6SGreg Roach ->delete(); 367a25f0a04SGreg Roach } else { 3681ad2dde6SGreg Roach DB::table('link')->where('l_file', '=', $this->id)->delete(); 3691ad2dde6SGreg Roach DB::table('media_file')->where('m_file', '=', $this->id)->delete(); 3701ad2dde6SGreg Roach DB::table('media')->where('m_file', '=', $this->id)->delete(); 371a25f0a04SGreg Roach } 372a25f0a04SGreg Roach } 373a25f0a04SGreg Roach 374a25f0a04SGreg Roach /** 375a25f0a04SGreg Roach * Export the tree to a GEDCOM file 376a25f0a04SGreg Roach * 3775792757eSGreg Roach * @param resource $stream 378b7e60af1SGreg Roach * 379b7e60af1SGreg Roach * @return void 380a25f0a04SGreg Roach */ 381425af8b9SGreg Roach public function exportGedcom($stream): void 382c1010edaSGreg Roach { 383a3d8780cSGreg Roach $buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this, 'UTF-8')); 38494026f20SGreg Roach 38594026f20SGreg Roach $union_families = DB::table('families') 38694026f20SGreg Roach ->where('f_file', '=', $this->id) 387a69f5655SGreg Roach ->select(['f_gedcom AS gedcom', 'f_id AS xref', new Expression('LENGTH(f_id) AS len'), new Expression('2 AS n')]); 38894026f20SGreg Roach 38994026f20SGreg Roach $union_sources = DB::table('sources') 39094026f20SGreg Roach ->where('s_file', '=', $this->id) 391a69f5655SGreg Roach ->select(['s_gedcom AS gedcom', 's_id AS xref', new Expression('LENGTH(s_id) AS len'), new Expression('3 AS n')]); 39294026f20SGreg Roach 39394026f20SGreg Roach $union_other = DB::table('other') 39494026f20SGreg Roach ->where('o_file', '=', $this->id) 3951635452cSGreg Roach ->whereNotIn('o_type', [Header::RECORD_TYPE, 'TRLR']) 396a69f5655SGreg Roach ->select(['o_gedcom AS gedcom', 'o_id AS xref', new Expression('LENGTH(o_id) AS len'), new Expression('4 AS n')]); 39794026f20SGreg Roach 39894026f20SGreg Roach $union_media = DB::table('media') 39994026f20SGreg Roach ->where('m_file', '=', $this->id) 400a69f5655SGreg Roach ->select(['m_gedcom AS gedcom', 'm_id AS xref', new Expression('LENGTH(m_id) AS len'), new Expression('5 AS n')]); 40194026f20SGreg Roach 402e5a6b4d4SGreg Roach DB::table('individuals') 40394026f20SGreg Roach ->where('i_file', '=', $this->id) 404a69f5655SGreg Roach ->select(['i_gedcom AS gedcom', 'i_id AS xref', new Expression('LENGTH(i_id) AS len'), new Expression('1 AS n')]) 40594026f20SGreg Roach ->union($union_families) 40694026f20SGreg Roach ->union($union_sources) 40794026f20SGreg Roach ->union($union_other) 40894026f20SGreg Roach ->union($union_media) 40994026f20SGreg Roach ->orderBy('n') 41094026f20SGreg Roach ->orderBy('len') 41194026f20SGreg Roach ->orderBy('xref') 41227825e0aSGreg Roach ->chunk(1000, static function (Collection $rows) use ($stream, &$buffer): void { 41394026f20SGreg Roach foreach ($rows as $row) { 4143d7a8a4cSGreg Roach $buffer .= FunctionsExport::reformatRecord($row->gedcom); 415a25f0a04SGreg Roach if (strlen($buffer) > 65535) { 4165792757eSGreg Roach fwrite($stream, $buffer); 417a25f0a04SGreg Roach $buffer = ''; 418a25f0a04SGreg Roach } 419a25f0a04SGreg Roach } 42094026f20SGreg Roach }); 42194026f20SGreg Roach 4220f471f91SGreg Roach fwrite($stream, $buffer . '0 TRLR' . Gedcom::EOL); 423a25f0a04SGreg Roach } 424a25f0a04SGreg Roach 425a25f0a04SGreg Roach /** 426a25f0a04SGreg Roach * Import data from a gedcom file into this tree. 427a25f0a04SGreg Roach * 4286ccdf4f0SGreg Roach * @param StreamInterface $stream The GEDCOM file. 429a25f0a04SGreg Roach * @param string $filename The preferred filename, for export/download. 430a25f0a04SGreg Roach * 431b7e60af1SGreg Roach * @return void 432a25f0a04SGreg Roach */ 4336ccdf4f0SGreg Roach public function importGedcomFile(StreamInterface $stream, string $filename): void 434c1010edaSGreg Roach { 435a25f0a04SGreg Roach // Read the file in blocks of roughly 64K. Ensure that each block 436a25f0a04SGreg Roach // contains complete gedcom records. This will ensure we don’t split 437a25f0a04SGreg Roach // multi-byte characters, as well as simplifying the code to import 438a25f0a04SGreg Roach // each block. 439a25f0a04SGreg Roach 440a25f0a04SGreg Roach $file_data = ''; 441a25f0a04SGreg Roach 442b7e60af1SGreg Roach $this->deleteGenealogyData((bool) $this->getPreference('keep_media')); 443a25f0a04SGreg Roach $this->setPreference('gedcom_filename', $filename); 444a25f0a04SGreg Roach $this->setPreference('imported', '0'); 445a25f0a04SGreg Roach 4466ccdf4f0SGreg Roach while (!$stream->eof()) { 4476ccdf4f0SGreg Roach $file_data .= $stream->read(65536); 448a25f0a04SGreg Roach // There is no strrpos() function that searches for substrings :-( 449a25f0a04SGreg Roach for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) { 450a25f0a04SGreg Roach if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) { 451a25f0a04SGreg Roach // We’ve found the last record boundary in this chunk of data 452a25f0a04SGreg Roach break; 453a25f0a04SGreg Roach } 454a25f0a04SGreg Roach } 455a25f0a04SGreg Roach if ($pos) { 4561ad2dde6SGreg Roach DB::table('gedcom_chunk')->insert([ 4571ad2dde6SGreg Roach 'gedcom_id' => $this->id, 4581ad2dde6SGreg Roach 'chunk_data' => substr($file_data, 0, $pos), 459c1010edaSGreg Roach ]); 4601ad2dde6SGreg Roach 461a25f0a04SGreg Roach $file_data = substr($file_data, $pos); 462a25f0a04SGreg Roach } 463a25f0a04SGreg Roach } 4641ad2dde6SGreg Roach DB::table('gedcom_chunk')->insert([ 4651ad2dde6SGreg Roach 'gedcom_id' => $this->id, 4661ad2dde6SGreg Roach 'chunk_data' => $file_data, 467c1010edaSGreg Roach ]); 468a25f0a04SGreg Roach 4696ccdf4f0SGreg Roach $stream->close(); 4706ccdf4f0SGreg Roach } 4716ccdf4f0SGreg Roach 4726ccdf4f0SGreg Roach /** 4736ccdf4f0SGreg Roach * Create a new record from GEDCOM data. 4746ccdf4f0SGreg Roach * 4756ccdf4f0SGreg Roach * @param string $gedcom 4766ccdf4f0SGreg Roach * 477*0d15532eSGreg Roach * @return GedcomRecord|Individual|Family|Location|Note|Source|Repository|Media|Submitter|Submission 4786ccdf4f0SGreg Roach * @throws InvalidArgumentException 4796ccdf4f0SGreg Roach */ 4806ccdf4f0SGreg Roach public function createRecord(string $gedcom): GedcomRecord 4816ccdf4f0SGreg Roach { 4826ccdf4f0SGreg Roach if (!Str::startsWith($gedcom, '0 @@ ')) { 4836ccdf4f0SGreg Roach throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@'); 4846ccdf4f0SGreg Roach } 4856ccdf4f0SGreg Roach 4866ccdf4f0SGreg Roach $xref = $this->getNewXref(); 4876ccdf4f0SGreg Roach $gedcom = '0 @' . $xref . '@ ' . Str::after($gedcom, '0 @@ '); 4886ccdf4f0SGreg Roach 4896ccdf4f0SGreg Roach // Create a change record 49053432476SGreg Roach $today = strtoupper(date('d M Y')); 49153432476SGreg Roach $now = date('H:i:s'); 49253432476SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName(); 4936ccdf4f0SGreg Roach 4946ccdf4f0SGreg Roach // Create a pending change 4956ccdf4f0SGreg Roach DB::table('change')->insert([ 4966ccdf4f0SGreg Roach 'gedcom_id' => $this->id, 4976ccdf4f0SGreg Roach 'xref' => $xref, 4986ccdf4f0SGreg Roach 'old_gedcom' => '', 4996ccdf4f0SGreg Roach 'new_gedcom' => $gedcom, 5006ccdf4f0SGreg Roach 'user_id' => Auth::id(), 5016ccdf4f0SGreg Roach ]); 5026ccdf4f0SGreg Roach 5036ccdf4f0SGreg Roach // Accept this pending change 5047c4add84SGreg Roach if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS)) { 505a091ac74SGreg Roach $record = Factory::gedcomRecord()->new($xref, $gedcom, null, $this); 5066ccdf4f0SGreg Roach 50722e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 50822e73debSGreg Roach 50922e73debSGreg Roach return $record; 5106ccdf4f0SGreg Roach } 5116ccdf4f0SGreg Roach 512a091ac74SGreg Roach return Factory::gedcomRecord()->new($xref, '', $gedcom, $this); 513a25f0a04SGreg Roach } 514304f20d5SGreg Roach 515304f20d5SGreg Roach /** 516b90d8accSGreg Roach * Generate a new XREF, unique across all family trees 517b90d8accSGreg Roach * 518b90d8accSGreg Roach * @return string 519b90d8accSGreg Roach */ 520771ae10aSGreg Roach public function getNewXref(): string 521c1010edaSGreg Roach { 522963fbaeeSGreg Roach // Lock the row, so that only one new XREF may be generated at a time. 523963fbaeeSGreg Roach DB::table('site_setting') 524963fbaeeSGreg Roach ->where('setting_name', '=', 'next_xref') 525963fbaeeSGreg Roach ->lockForUpdate() 526963fbaeeSGreg Roach ->get(); 527963fbaeeSGreg Roach 528a214e186SGreg Roach $prefix = 'X'; 529b90d8accSGreg Roach 530971d66c8SGreg Roach $increment = 1.0; 531b90d8accSGreg Roach do { 532963fbaeeSGreg Roach $num = (int) Site::getPreference('next_xref') + (int) $increment; 533971d66c8SGreg Roach 534971d66c8SGreg Roach // This exponential increment allows us to scan over large blocks of 535971d66c8SGreg Roach // existing data in a reasonable time. 536971d66c8SGreg Roach $increment *= 1.01; 537963fbaeeSGreg Roach 538963fbaeeSGreg Roach $xref = $prefix . $num; 539963fbaeeSGreg Roach 540963fbaeeSGreg Roach // Records may already exist with this sequence number. 541963fbaeeSGreg Roach $already_used = 542963fbaeeSGreg Roach DB::table('individuals')->where('i_id', '=', $xref)->exists() || 543963fbaeeSGreg Roach DB::table('families')->where('f_id', '=', $xref)->exists() || 544963fbaeeSGreg Roach DB::table('sources')->where('s_id', '=', $xref)->exists() || 545963fbaeeSGreg Roach DB::table('media')->where('m_id', '=', $xref)->exists() || 546963fbaeeSGreg Roach DB::table('other')->where('o_id', '=', $xref)->exists() || 547963fbaeeSGreg Roach DB::table('change')->where('xref', '=', $xref)->exists(); 548963fbaeeSGreg Roach } while ($already_used); 549963fbaeeSGreg Roach 550963fbaeeSGreg Roach Site::setPreference('next_xref', (string) $num); 551b90d8accSGreg Roach 552a214e186SGreg Roach return $xref; 553b90d8accSGreg Roach } 554b90d8accSGreg Roach 555b90d8accSGreg Roach /** 556afb591d7SGreg Roach * Create a new family from GEDCOM data. 557afb591d7SGreg Roach * 558afb591d7SGreg Roach * @param string $gedcom 559afb591d7SGreg Roach * 560afb591d7SGreg Roach * @return Family 561afb591d7SGreg Roach * @throws InvalidArgumentException 562afb591d7SGreg Roach */ 563afb591d7SGreg Roach public function createFamily(string $gedcom): GedcomRecord 564afb591d7SGreg Roach { 565bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ FAM')) { 566afb591d7SGreg Roach throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM'); 567afb591d7SGreg Roach } 568afb591d7SGreg Roach 569afb591d7SGreg Roach $xref = $this->getNewXref(); 570bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ FAM' . Str::after($gedcom, '0 @@ FAM'); 571afb591d7SGreg Roach 572afb591d7SGreg Roach // Create a change record 57353432476SGreg Roach $today = strtoupper(date('d M Y')); 57453432476SGreg Roach $now = date('H:i:s'); 57553432476SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName(); 576afb591d7SGreg Roach 577afb591d7SGreg Roach // Create a pending change 578963fbaeeSGreg Roach DB::table('change')->insert([ 579963fbaeeSGreg Roach 'gedcom_id' => $this->id, 580963fbaeeSGreg Roach 'xref' => $xref, 581963fbaeeSGreg Roach 'old_gedcom' => '', 582963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 583963fbaeeSGreg Roach 'user_id' => Auth::id(), 584afb591d7SGreg Roach ]); 585304f20d5SGreg Roach 586304f20d5SGreg Roach // Accept this pending change 5877c4add84SGreg Roach if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') { 588a091ac74SGreg Roach $record = Factory::family()->new($xref, $gedcom, null, $this); 589afb591d7SGreg Roach 59022e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 59122e73debSGreg Roach 59222e73debSGreg Roach return $record; 593304f20d5SGreg Roach } 594afb591d7SGreg Roach 595a091ac74SGreg Roach return Factory::family()->new($xref, '', $gedcom, $this); 596afb591d7SGreg Roach } 597afb591d7SGreg Roach 598afb591d7SGreg Roach /** 599afb591d7SGreg Roach * Create a new individual from GEDCOM data. 600afb591d7SGreg Roach * 601afb591d7SGreg Roach * @param string $gedcom 602afb591d7SGreg Roach * 603afb591d7SGreg Roach * @return Individual 604afb591d7SGreg Roach * @throws InvalidArgumentException 605afb591d7SGreg Roach */ 606afb591d7SGreg Roach public function createIndividual(string $gedcom): GedcomRecord 607afb591d7SGreg Roach { 608bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ INDI')) { 609afb591d7SGreg Roach throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI'); 610afb591d7SGreg Roach } 611afb591d7SGreg Roach 612afb591d7SGreg Roach $xref = $this->getNewXref(); 613bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ INDI' . Str::after($gedcom, '0 @@ INDI'); 614afb591d7SGreg Roach 615afb591d7SGreg Roach // Create a change record 61653432476SGreg Roach $today = strtoupper(date('d M Y')); 61753432476SGreg Roach $now = date('H:i:s'); 61853432476SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName(); 619afb591d7SGreg Roach 620afb591d7SGreg Roach // Create a pending change 621963fbaeeSGreg Roach DB::table('change')->insert([ 622963fbaeeSGreg Roach 'gedcom_id' => $this->id, 623963fbaeeSGreg Roach 'xref' => $xref, 624963fbaeeSGreg Roach 'old_gedcom' => '', 625963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 626963fbaeeSGreg Roach 'user_id' => Auth::id(), 627afb591d7SGreg Roach ]); 628afb591d7SGreg Roach 629afb591d7SGreg Roach // Accept this pending change 6307c4add84SGreg Roach if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') { 631a091ac74SGreg Roach $record = Factory::individual()->new($xref, $gedcom, null, $this); 632afb591d7SGreg Roach 63322e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 63422e73debSGreg Roach 63522e73debSGreg Roach return $record; 636afb591d7SGreg Roach } 637afb591d7SGreg Roach 638a091ac74SGreg Roach return Factory::individual()->new($xref, '', $gedcom, $this); 639304f20d5SGreg Roach } 6408586983fSGreg Roach 6418586983fSGreg Roach /** 64220b58d20SGreg Roach * Create a new media object from GEDCOM data. 64320b58d20SGreg Roach * 64420b58d20SGreg Roach * @param string $gedcom 64520b58d20SGreg Roach * 64620b58d20SGreg Roach * @return Media 64720b58d20SGreg Roach * @throws InvalidArgumentException 64820b58d20SGreg Roach */ 64920b58d20SGreg Roach public function createMediaObject(string $gedcom): Media 65020b58d20SGreg Roach { 651bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ OBJE')) { 65220b58d20SGreg Roach throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE'); 65320b58d20SGreg Roach } 65420b58d20SGreg Roach 65520b58d20SGreg Roach $xref = $this->getNewXref(); 656bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ OBJE' . Str::after($gedcom, '0 @@ OBJE'); 65720b58d20SGreg Roach 65820b58d20SGreg Roach // Create a change record 65953432476SGreg Roach $today = strtoupper(date('d M Y')); 66053432476SGreg Roach $now = date('H:i:s'); 66153432476SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName(); 66220b58d20SGreg Roach 66320b58d20SGreg Roach // Create a pending change 664963fbaeeSGreg Roach DB::table('change')->insert([ 665963fbaeeSGreg Roach 'gedcom_id' => $this->id, 666963fbaeeSGreg Roach 'xref' => $xref, 667963fbaeeSGreg Roach 'old_gedcom' => '', 668963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 669963fbaeeSGreg Roach 'user_id' => Auth::id(), 67020b58d20SGreg Roach ]); 67120b58d20SGreg Roach 67220b58d20SGreg Roach // Accept this pending change 6737c4add84SGreg Roach if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') { 674a091ac74SGreg Roach $record = Factory::media()->new($xref, $gedcom, null, $this); 67520b58d20SGreg Roach 67622e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 67722e73debSGreg Roach 67822e73debSGreg Roach return $record; 67920b58d20SGreg Roach } 68020b58d20SGreg Roach 681a091ac74SGreg Roach return Factory::media()->new($xref, '', $gedcom, $this); 68220b58d20SGreg Roach } 68320b58d20SGreg Roach 68420b58d20SGreg Roach /** 6858586983fSGreg Roach * What is the most significant individual in this tree. 6868586983fSGreg Roach * 687e5a6b4d4SGreg Roach * @param UserInterface $user 6883370567dSGreg Roach * @param string $xref 6898586983fSGreg Roach * 6908586983fSGreg Roach * @return Individual 6918586983fSGreg Roach */ 6923370567dSGreg Roach public function significantIndividual(UserInterface $user, $xref = ''): Individual 693c1010edaSGreg Roach { 6943370567dSGreg Roach if ($xref === '') { 6958f9b0fb2SGreg Roach $individual = null; 6963370567dSGreg Roach } else { 697a091ac74SGreg Roach $individual = Factory::individual()->make($xref, $this); 6983370567dSGreg Roach 6993370567dSGreg Roach if ($individual === null) { 700a091ac74SGreg Roach $family = Factory::family()->make($xref, $this); 7013370567dSGreg Roach 7023370567dSGreg Roach if ($family instanceof Family) { 7033370567dSGreg Roach $individual = $family->spouses()->first() ?? $family->children()->first(); 7043370567dSGreg Roach } 7053370567dSGreg Roach } 7063370567dSGreg Roach } 7078586983fSGreg Roach 7086e91273cSGreg Roach if ($individual === null && $this->getUserPreference($user, User::PREF_TREE_DEFAULT_XREF) !== '') { 709a091ac74SGreg Roach $individual = Factory::individual()->make($this->getUserPreference($user, User::PREF_TREE_DEFAULT_XREF), $this); 7108586983fSGreg Roach } 7118f9b0fb2SGreg Roach 7127c4add84SGreg Roach if ($individual === null && $this->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF) !== '') { 713a091ac74SGreg Roach $individual = Factory::individual()->make($this->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF), $this); 7148586983fSGreg Roach } 7158f9b0fb2SGreg Roach 716bec87e94SGreg Roach if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') { 717a091ac74SGreg Roach $individual = Factory::individual()->make($this->getPreference('PEDIGREE_ROOT_ID'), $this); 7188586983fSGreg Roach } 7198f9b0fb2SGreg Roach if ($individual === null) { 7208f9b0fb2SGreg Roach $xref = (string) DB::table('individuals') 7218f9b0fb2SGreg Roach ->where('i_file', '=', $this->id()) 7228f9b0fb2SGreg Roach ->min('i_id'); 723769d7d6eSGreg Roach 724a091ac74SGreg Roach $individual = Factory::individual()->make($xref, $this); 7255fe1add5SGreg Roach } 7268f9b0fb2SGreg Roach if ($individual === null) { 7275fe1add5SGreg Roach // always return a record 728a091ac74SGreg Roach $individual = Factory::individual()->new('I', '0 @I@ INDI', null, $this); 7295fe1add5SGreg Roach } 7305fe1add5SGreg Roach 7315fe1add5SGreg Roach return $individual; 7325fe1add5SGreg Roach } 7331df7ae39SGreg Roach 73485a166d8SGreg Roach /** 73585a166d8SGreg Roach * Where do we store our media files. 73685a166d8SGreg Roach * 737a04bb9a2SGreg Roach * @param FilesystemInterface $data_filesystem 738a04bb9a2SGreg Roach * 73985a166d8SGreg Roach * @return FilesystemInterface 74085a166d8SGreg Roach */ 741a04bb9a2SGreg Roach public function mediaFilesystem(FilesystemInterface $data_filesystem): FilesystemInterface 7421df7ae39SGreg Roach { 743456d0d35SGreg Roach $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/'); 744a04bb9a2SGreg Roach $adapter = new ChrootAdapter($data_filesystem, $media_dir); 745456d0d35SGreg Roach 746456d0d35SGreg Roach return new Filesystem($adapter); 7471df7ae39SGreg Roach } 748a25f0a04SGreg Roach} 749