1a25f0a04SGreg Roach<?php 23976b470SGreg Roach 3a25f0a04SGreg Roach/** 4a25f0a04SGreg Roach * webtrees: online genealogy 58fcd0d32SGreg Roach * Copyright (C) 2019 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; 275afbc57aSGreg Roachuse Fisharebest\Webtrees\Services\TreeService; 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; 391e653452SGreg Roach 40a25f0a04SGreg Roach/** 4176692c8bSGreg Roach * Provide an interface to the wt_gedcom table. 42a25f0a04SGreg Roach */ 43c1010edaSGreg Roachclass Tree 44c1010edaSGreg Roach{ 45061b43d7SGreg Roach private const RESN_PRIVACY = [ 46061b43d7SGreg Roach 'none' => Auth::PRIV_PRIVATE, 47061b43d7SGreg Roach 'privacy' => Auth::PRIV_USER, 48061b43d7SGreg Roach 'confidential' => Auth::PRIV_NONE, 49061b43d7SGreg Roach 'hidden' => Auth::PRIV_HIDE, 50061b43d7SGreg Roach ]; 513df1e584SGreg Roach 526ccdf4f0SGreg Roach /** @var int The tree's ID number */ 536ccdf4f0SGreg Roach private $id; 543df1e584SGreg Roach 556ccdf4f0SGreg Roach /** @var string The tree's name */ 566ccdf4f0SGreg Roach private $name; 573df1e584SGreg Roach 586ccdf4f0SGreg Roach /** @var string The tree's title */ 596ccdf4f0SGreg Roach private $title; 603df1e584SGreg Roach 616ccdf4f0SGreg Roach /** @var int[] Default access rules for facts in this tree */ 626ccdf4f0SGreg Roach private $fact_privacy; 633df1e584SGreg Roach 646ccdf4f0SGreg Roach /** @var int[] Default access rules for individuals in this tree */ 656ccdf4f0SGreg Roach private $individual_privacy; 663df1e584SGreg Roach 676ccdf4f0SGreg Roach /** @var integer[][] Default access rules for individual facts in this tree */ 686ccdf4f0SGreg Roach private $individual_fact_privacy; 693df1e584SGreg Roach 706ccdf4f0SGreg Roach /** @var string[] Cached copy of the wt_gedcom_setting table. */ 716ccdf4f0SGreg Roach private $preferences = []; 723df1e584SGreg Roach 736ccdf4f0SGreg Roach /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */ 746ccdf4f0SGreg Roach private $user_preferences = []; 75061b43d7SGreg Roach 76a25f0a04SGreg Roach /** 773df1e584SGreg Roach * Create a tree object. 78a25f0a04SGreg Roach * 7972cf66d4SGreg Roach * @param int $id 80aa6f03bbSGreg Roach * @param string $name 81cc13d6d8SGreg Roach * @param string $title 82a25f0a04SGreg Roach */ 835afbc57aSGreg Roach public function __construct(int $id, string $name, string $title) 84c1010edaSGreg Roach { 8572cf66d4SGreg Roach $this->id = $id; 86aa6f03bbSGreg Roach $this->name = $name; 87cc13d6d8SGreg Roach $this->title = $title; 8813abd6f3SGreg Roach $this->fact_privacy = []; 8913abd6f3SGreg Roach $this->individual_privacy = []; 9013abd6f3SGreg Roach $this->individual_fact_privacy = []; 91518bbdc1SGreg Roach 92518bbdc1SGreg Roach // Load the privacy settings for this tree 93061b43d7SGreg Roach $rows = DB::table('default_resn') 94061b43d7SGreg Roach ->where('gedcom_id', '=', $this->id) 95061b43d7SGreg Roach ->get(); 96518bbdc1SGreg Roach 97518bbdc1SGreg Roach foreach ($rows as $row) { 98061b43d7SGreg Roach // Convert GEDCOM privacy restriction to a webtrees access level. 99061b43d7SGreg Roach $row->resn = self::RESN_PRIVACY[$row->resn]; 100061b43d7SGreg Roach 101518bbdc1SGreg Roach if ($row->xref !== null) { 102518bbdc1SGreg Roach if ($row->tag_type !== null) { 103b262b3d3SGreg Roach $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn; 104518bbdc1SGreg Roach } else { 105b262b3d3SGreg Roach $this->individual_privacy[$row->xref] = $row->resn; 106518bbdc1SGreg Roach } 107518bbdc1SGreg Roach } else { 108b262b3d3SGreg Roach $this->fact_privacy[$row->tag_type] = $row->resn; 109518bbdc1SGreg Roach } 110518bbdc1SGreg Roach } 111a25f0a04SGreg Roach } 112a25f0a04SGreg Roach 113a25f0a04SGreg Roach /** 1145afbc57aSGreg Roach * A closure which will create a record from a database row. 1155afbc57aSGreg Roach * 1165afbc57aSGreg Roach * @return Closure 1175afbc57aSGreg Roach */ 1185afbc57aSGreg Roach public static function rowMapper(): Closure 1195afbc57aSGreg Roach { 1205afbc57aSGreg Roach return static function (stdClass $row): Tree { 1215afbc57aSGreg Roach return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title); 1225afbc57aSGreg Roach }; 1235afbc57aSGreg Roach } 1245afbc57aSGreg Roach 1255afbc57aSGreg Roach /** 1266ccdf4f0SGreg Roach * Find the tree with a specific ID. 127a25f0a04SGreg Roach * 1286ccdf4f0SGreg Roach * @param int $tree_id 1296ccdf4f0SGreg Roach * 1306ccdf4f0SGreg Roach * @return Tree 1311e653452SGreg Roach * @deprecated 132a25f0a04SGreg Roach */ 1336ccdf4f0SGreg Roach public static function findById(int $tree_id): Tree 134c1010edaSGreg Roach { 1351e653452SGreg Roach return app(TreeService::class)->all()->first(static function (Tree $tree) use ($tree_id): bool { 1361e653452SGreg Roach return $tree->id() === $tree_id; 1371e653452SGreg Roach }); 1386ccdf4f0SGreg Roach } 1396ccdf4f0SGreg Roach 1406ccdf4f0SGreg Roach /** 1416ccdf4f0SGreg Roach * Set the tree’s configuration settings. 1426ccdf4f0SGreg Roach * 1436ccdf4f0SGreg Roach * @param string $setting_name 1446ccdf4f0SGreg Roach * @param string $setting_value 1456ccdf4f0SGreg Roach * 1466ccdf4f0SGreg Roach * @return $this 1476ccdf4f0SGreg Roach */ 1486ccdf4f0SGreg Roach public function setPreference(string $setting_name, string $setting_value): Tree 1496ccdf4f0SGreg Roach { 1506ccdf4f0SGreg Roach if ($setting_value !== $this->getPreference($setting_name)) { 1516ccdf4f0SGreg Roach DB::table('gedcom_setting')->updateOrInsert([ 1526ccdf4f0SGreg Roach 'gedcom_id' => $this->id, 1536ccdf4f0SGreg Roach 'setting_name' => $setting_name, 1546ccdf4f0SGreg Roach ], [ 1556ccdf4f0SGreg Roach 'setting_value' => $setting_value, 1566ccdf4f0SGreg Roach ]); 1576ccdf4f0SGreg Roach 1586ccdf4f0SGreg Roach $this->preferences[$setting_name] = $setting_value; 1596ccdf4f0SGreg Roach 1606ccdf4f0SGreg Roach Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this); 1616ccdf4f0SGreg Roach } 1626ccdf4f0SGreg Roach 1636ccdf4f0SGreg Roach return $this; 1646ccdf4f0SGreg Roach } 1656ccdf4f0SGreg Roach 1666ccdf4f0SGreg Roach /** 1676ccdf4f0SGreg Roach * Get the tree’s configuration settings. 1686ccdf4f0SGreg Roach * 1696ccdf4f0SGreg Roach * @param string $setting_name 1706ccdf4f0SGreg Roach * @param string $default 1716ccdf4f0SGreg Roach * 1726ccdf4f0SGreg Roach * @return string 1736ccdf4f0SGreg Roach */ 1746ccdf4f0SGreg Roach public function getPreference(string $setting_name, string $default = ''): string 1756ccdf4f0SGreg Roach { 17654c1ab5eSGreg Roach if ($this->preferences === []) { 1776ccdf4f0SGreg Roach $this->preferences = DB::table('gedcom_setting') 1786ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 1796ccdf4f0SGreg Roach ->pluck('setting_value', 'setting_name') 1806ccdf4f0SGreg Roach ->all(); 1816ccdf4f0SGreg Roach } 1826ccdf4f0SGreg Roach 1836ccdf4f0SGreg Roach return $this->preferences[$setting_name] ?? $default; 1846ccdf4f0SGreg Roach } 1856ccdf4f0SGreg Roach 1866ccdf4f0SGreg Roach /** 1876ccdf4f0SGreg Roach * The name of this tree 1886ccdf4f0SGreg Roach * 1896ccdf4f0SGreg Roach * @return string 1906ccdf4f0SGreg Roach */ 1916ccdf4f0SGreg Roach public function name(): string 1926ccdf4f0SGreg Roach { 1936ccdf4f0SGreg Roach return $this->name; 1946ccdf4f0SGreg Roach } 1956ccdf4f0SGreg Roach 1966ccdf4f0SGreg Roach /** 1976ccdf4f0SGreg Roach * The title of this tree 1986ccdf4f0SGreg Roach * 1996ccdf4f0SGreg Roach * @return string 2006ccdf4f0SGreg Roach */ 2016ccdf4f0SGreg Roach public function title(): string 2026ccdf4f0SGreg Roach { 2036ccdf4f0SGreg Roach return $this->title; 2046ccdf4f0SGreg Roach } 2056ccdf4f0SGreg Roach 2066ccdf4f0SGreg Roach /** 2076ccdf4f0SGreg Roach * The fact-level privacy for this tree. 2086ccdf4f0SGreg Roach * 2096ccdf4f0SGreg Roach * @return int[] 2106ccdf4f0SGreg Roach */ 2116ccdf4f0SGreg Roach public function getFactPrivacy(): array 2126ccdf4f0SGreg Roach { 2136ccdf4f0SGreg Roach return $this->fact_privacy; 2146ccdf4f0SGreg Roach } 2156ccdf4f0SGreg Roach 2166ccdf4f0SGreg Roach /** 2176ccdf4f0SGreg Roach * The individual-level privacy for this tree. 2186ccdf4f0SGreg Roach * 2196ccdf4f0SGreg Roach * @return int[] 2206ccdf4f0SGreg Roach */ 2216ccdf4f0SGreg Roach public function getIndividualPrivacy(): array 2226ccdf4f0SGreg Roach { 2236ccdf4f0SGreg Roach return $this->individual_privacy; 2246ccdf4f0SGreg Roach } 2256ccdf4f0SGreg Roach 2266ccdf4f0SGreg Roach /** 2276ccdf4f0SGreg Roach * The individual-fact-level privacy for this tree. 2286ccdf4f0SGreg Roach * 2296ccdf4f0SGreg Roach * @return int[][] 2306ccdf4f0SGreg Roach */ 2316ccdf4f0SGreg Roach public function getIndividualFactPrivacy(): array 2326ccdf4f0SGreg Roach { 2336ccdf4f0SGreg Roach return $this->individual_fact_privacy; 2346ccdf4f0SGreg Roach } 2356ccdf4f0SGreg Roach 2366ccdf4f0SGreg Roach /** 2376ccdf4f0SGreg Roach * Set the tree’s user-configuration settings. 2386ccdf4f0SGreg Roach * 2396ccdf4f0SGreg Roach * @param UserInterface $user 2406ccdf4f0SGreg Roach * @param string $setting_name 2416ccdf4f0SGreg Roach * @param string $setting_value 2426ccdf4f0SGreg Roach * 2436ccdf4f0SGreg Roach * @return $this 2446ccdf4f0SGreg Roach */ 2456ccdf4f0SGreg Roach public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree 2466ccdf4f0SGreg Roach { 2476ccdf4f0SGreg Roach if ($this->getUserPreference($user, $setting_name) !== $setting_value) { 2486ccdf4f0SGreg Roach // Update the database 2496ccdf4f0SGreg Roach DB::table('user_gedcom_setting')->updateOrInsert([ 2506ccdf4f0SGreg Roach 'gedcom_id' => $this->id(), 2516ccdf4f0SGreg Roach 'user_id' => $user->id(), 2526ccdf4f0SGreg Roach 'setting_name' => $setting_name, 2536ccdf4f0SGreg Roach ], [ 2546ccdf4f0SGreg Roach 'setting_value' => $setting_value, 2556ccdf4f0SGreg Roach ]); 2566ccdf4f0SGreg Roach 2576ccdf4f0SGreg Roach // Update the cache 2586ccdf4f0SGreg Roach $this->user_preferences[$user->id()][$setting_name] = $setting_value; 2596ccdf4f0SGreg Roach // Audit log of changes 2606ccdf4f0SGreg Roach Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this); 2616ccdf4f0SGreg Roach } 2626ccdf4f0SGreg Roach 2636ccdf4f0SGreg Roach return $this; 2646ccdf4f0SGreg Roach } 2656ccdf4f0SGreg Roach 2666ccdf4f0SGreg Roach /** 2676ccdf4f0SGreg Roach * Get the tree’s user-configuration settings. 2686ccdf4f0SGreg Roach * 2696ccdf4f0SGreg Roach * @param UserInterface $user 2706ccdf4f0SGreg Roach * @param string $setting_name 2716ccdf4f0SGreg Roach * @param string $default 2726ccdf4f0SGreg Roach * 2736ccdf4f0SGreg Roach * @return string 2746ccdf4f0SGreg Roach */ 2756ccdf4f0SGreg Roach public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string 2766ccdf4f0SGreg Roach { 2776ccdf4f0SGreg Roach // There are lots of settings, and we need to fetch lots of them on every page 2786ccdf4f0SGreg Roach // so it is quicker to fetch them all in one go. 2796ccdf4f0SGreg Roach if (!array_key_exists($user->id(), $this->user_preferences)) { 2806ccdf4f0SGreg Roach $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting') 2816ccdf4f0SGreg Roach ->where('user_id', '=', $user->id()) 2826ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 2836ccdf4f0SGreg Roach ->pluck('setting_value', 'setting_name') 2846ccdf4f0SGreg Roach ->all(); 2856ccdf4f0SGreg Roach } 2866ccdf4f0SGreg Roach 2876ccdf4f0SGreg Roach return $this->user_preferences[$user->id()][$setting_name] ?? $default; 2886ccdf4f0SGreg Roach } 2896ccdf4f0SGreg Roach 2906ccdf4f0SGreg Roach /** 2916ccdf4f0SGreg Roach * The ID of this tree 2926ccdf4f0SGreg Roach * 2936ccdf4f0SGreg Roach * @return int 2946ccdf4f0SGreg Roach */ 2956ccdf4f0SGreg Roach public function id(): int 2966ccdf4f0SGreg Roach { 2976ccdf4f0SGreg Roach return $this->id; 2986ccdf4f0SGreg Roach } 2996ccdf4f0SGreg Roach 3006ccdf4f0SGreg Roach /** 3016ccdf4f0SGreg Roach * Can a user accept changes for this tree? 3026ccdf4f0SGreg Roach * 3036ccdf4f0SGreg Roach * @param UserInterface $user 3046ccdf4f0SGreg Roach * 3056ccdf4f0SGreg Roach * @return bool 3066ccdf4f0SGreg Roach */ 3076ccdf4f0SGreg Roach public function canAcceptChanges(UserInterface $user): bool 3086ccdf4f0SGreg Roach { 3096ccdf4f0SGreg Roach return Auth::isModerator($this, $user); 3106ccdf4f0SGreg Roach } 3116ccdf4f0SGreg Roach 3126ccdf4f0SGreg Roach /** 313b78374c5SGreg Roach * Are there any pending edits for this tree, than need reviewing by a moderator. 314b78374c5SGreg Roach * 315b78374c5SGreg Roach * @return bool 316b78374c5SGreg Roach */ 317771ae10aSGreg Roach public function hasPendingEdit(): bool 318c1010edaSGreg Roach { 31915a3f100SGreg Roach return DB::table('change') 32015a3f100SGreg Roach ->where('gedcom_id', '=', $this->id) 32115a3f100SGreg Roach ->where('status', '=', 'pending') 32215a3f100SGreg Roach ->exists(); 323b78374c5SGreg Roach } 324b78374c5SGreg Roach 325b78374c5SGreg Roach /** 3266ccdf4f0SGreg Roach * Delete everything relating to a tree 3276ccdf4f0SGreg Roach * 3286ccdf4f0SGreg Roach * @return void 3296ccdf4f0SGreg Roach */ 3306ccdf4f0SGreg Roach public function delete(): void 3316ccdf4f0SGreg Roach { 3326ccdf4f0SGreg Roach // If this is the default tree, then unset it 3336ccdf4f0SGreg Roach if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) { 3346ccdf4f0SGreg Roach Site::setPreference('DEFAULT_GEDCOM', ''); 3356ccdf4f0SGreg Roach } 3366ccdf4f0SGreg Roach 3376ccdf4f0SGreg Roach $this->deleteGenealogyData(false); 3386ccdf4f0SGreg Roach 3396ccdf4f0SGreg Roach DB::table('block_setting') 3406ccdf4f0SGreg Roach ->join('block', 'block.block_id', '=', 'block_setting.block_id') 3416ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 3426ccdf4f0SGreg Roach ->delete(); 3436ccdf4f0SGreg Roach DB::table('block')->where('gedcom_id', '=', $this->id)->delete(); 3446ccdf4f0SGreg Roach DB::table('user_gedcom_setting')->where('gedcom_id', '=', $this->id)->delete(); 3456ccdf4f0SGreg Roach DB::table('gedcom_setting')->where('gedcom_id', '=', $this->id)->delete(); 3466ccdf4f0SGreg Roach DB::table('module_privacy')->where('gedcom_id', '=', $this->id)->delete(); 3476ccdf4f0SGreg Roach DB::table('hit_counter')->where('gedcom_id', '=', $this->id)->delete(); 3486ccdf4f0SGreg Roach DB::table('default_resn')->where('gedcom_id', '=', $this->id)->delete(); 3496ccdf4f0SGreg Roach DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete(); 3506ccdf4f0SGreg Roach DB::table('log')->where('gedcom_id', '=', $this->id)->delete(); 3516ccdf4f0SGreg Roach DB::table('gedcom')->where('gedcom_id', '=', $this->id)->delete(); 3526ccdf4f0SGreg Roach } 3536ccdf4f0SGreg Roach 3546ccdf4f0SGreg Roach /** 355a25f0a04SGreg Roach * Delete all the genealogy data from a tree - in preparation for importing 356a25f0a04SGreg Roach * new data. Optionally retain the media data, for when the user has been 357a25f0a04SGreg Roach * editing their data offline using an application which deletes (or does not 358a25f0a04SGreg Roach * support) media data. 359a25f0a04SGreg Roach * 360a25f0a04SGreg Roach * @param bool $keep_media 361b7e60af1SGreg Roach * 362b7e60af1SGreg Roach * @return void 363a25f0a04SGreg Roach */ 364e364afe4SGreg Roach public function deleteGenealogyData(bool $keep_media): void 365c1010edaSGreg Roach { 3661ad2dde6SGreg Roach DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete(); 3671ad2dde6SGreg Roach DB::table('individuals')->where('i_file', '=', $this->id)->delete(); 3681ad2dde6SGreg Roach DB::table('families')->where('f_file', '=', $this->id)->delete(); 3691ad2dde6SGreg Roach DB::table('sources')->where('s_file', '=', $this->id)->delete(); 3701ad2dde6SGreg Roach DB::table('other')->where('o_file', '=', $this->id)->delete(); 3711ad2dde6SGreg Roach DB::table('places')->where('p_file', '=', $this->id)->delete(); 3721ad2dde6SGreg Roach DB::table('placelinks')->where('pl_file', '=', $this->id)->delete(); 3731ad2dde6SGreg Roach DB::table('name')->where('n_file', '=', $this->id)->delete(); 3741ad2dde6SGreg Roach DB::table('dates')->where('d_file', '=', $this->id)->delete(); 3751ad2dde6SGreg Roach DB::table('change')->where('gedcom_id', '=', $this->id)->delete(); 376a25f0a04SGreg Roach 377a25f0a04SGreg Roach if ($keep_media) { 3781ad2dde6SGreg Roach DB::table('link')->where('l_file', '=', $this->id) 3791ad2dde6SGreg Roach ->where('l_type', '<>', 'OBJE') 3801ad2dde6SGreg Roach ->delete(); 381a25f0a04SGreg Roach } else { 3821ad2dde6SGreg Roach DB::table('link')->where('l_file', '=', $this->id)->delete(); 3831ad2dde6SGreg Roach DB::table('media_file')->where('m_file', '=', $this->id)->delete(); 3841ad2dde6SGreg Roach DB::table('media')->where('m_file', '=', $this->id)->delete(); 385a25f0a04SGreg Roach } 386a25f0a04SGreg Roach } 387a25f0a04SGreg Roach 388a25f0a04SGreg Roach /** 389a25f0a04SGreg Roach * Export the tree to a GEDCOM file 390a25f0a04SGreg Roach * 3915792757eSGreg Roach * @param resource $stream 392b7e60af1SGreg Roach * 393b7e60af1SGreg Roach * @return void 394a25f0a04SGreg Roach */ 395425af8b9SGreg Roach public function exportGedcom($stream): void 396c1010edaSGreg Roach { 397a3d8780cSGreg Roach $buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this, 'UTF-8')); 39894026f20SGreg Roach 39994026f20SGreg Roach $union_families = DB::table('families') 40094026f20SGreg Roach ->where('f_file', '=', $this->id) 401a69f5655SGreg Roach ->select(['f_gedcom AS gedcom', 'f_id AS xref', new Expression('LENGTH(f_id) AS len'), new Expression('2 AS n')]); 40294026f20SGreg Roach 40394026f20SGreg Roach $union_sources = DB::table('sources') 40494026f20SGreg Roach ->where('s_file', '=', $this->id) 405a69f5655SGreg Roach ->select(['s_gedcom AS gedcom', 's_id AS xref', new Expression('LENGTH(s_id) AS len'), new Expression('3 AS n')]); 40694026f20SGreg Roach 40794026f20SGreg Roach $union_other = DB::table('other') 40894026f20SGreg Roach ->where('o_file', '=', $this->id) 40994026f20SGreg Roach ->whereNotIn('o_type', ['HEAD', 'TRLR']) 410a69f5655SGreg Roach ->select(['o_gedcom AS gedcom', 'o_id AS xref', new Expression('LENGTH(o_id) AS len'), new Expression('4 AS n')]); 41194026f20SGreg Roach 41294026f20SGreg Roach $union_media = DB::table('media') 41394026f20SGreg Roach ->where('m_file', '=', $this->id) 414a69f5655SGreg Roach ->select(['m_gedcom AS gedcom', 'm_id AS xref', new Expression('LENGTH(m_id) AS len'), new Expression('5 AS n')]); 41594026f20SGreg Roach 416e5a6b4d4SGreg Roach DB::table('individuals') 41794026f20SGreg Roach ->where('i_file', '=', $this->id) 418a69f5655SGreg Roach ->select(['i_gedcom AS gedcom', 'i_id AS xref', new Expression('LENGTH(i_id) AS len'), new Expression('1 AS n')]) 41994026f20SGreg Roach ->union($union_families) 42094026f20SGreg Roach ->union($union_sources) 42194026f20SGreg Roach ->union($union_other) 42294026f20SGreg Roach ->union($union_media) 42394026f20SGreg Roach ->orderBy('n') 42494026f20SGreg Roach ->orderBy('len') 42594026f20SGreg Roach ->orderBy('xref') 42627825e0aSGreg Roach ->chunk(1000, static function (Collection $rows) use ($stream, &$buffer): void { 42794026f20SGreg Roach foreach ($rows as $row) { 4283d7a8a4cSGreg Roach $buffer .= FunctionsExport::reformatRecord($row->gedcom); 429a25f0a04SGreg Roach if (strlen($buffer) > 65535) { 4305792757eSGreg Roach fwrite($stream, $buffer); 431a25f0a04SGreg Roach $buffer = ''; 432a25f0a04SGreg Roach } 433a25f0a04SGreg Roach } 43494026f20SGreg Roach }); 43594026f20SGreg Roach 4360f471f91SGreg Roach fwrite($stream, $buffer . '0 TRLR' . Gedcom::EOL); 437a25f0a04SGreg Roach } 438a25f0a04SGreg Roach 439a25f0a04SGreg Roach /** 440a25f0a04SGreg Roach * Import data from a gedcom file into this tree. 441a25f0a04SGreg Roach * 4426ccdf4f0SGreg Roach * @param StreamInterface $stream The GEDCOM file. 443a25f0a04SGreg Roach * @param string $filename The preferred filename, for export/download. 444a25f0a04SGreg Roach * 445b7e60af1SGreg Roach * @return void 446a25f0a04SGreg Roach */ 4476ccdf4f0SGreg Roach public function importGedcomFile(StreamInterface $stream, string $filename): void 448c1010edaSGreg Roach { 449a25f0a04SGreg Roach // Read the file in blocks of roughly 64K. Ensure that each block 450a25f0a04SGreg Roach // contains complete gedcom records. This will ensure we don’t split 451a25f0a04SGreg Roach // multi-byte characters, as well as simplifying the code to import 452a25f0a04SGreg Roach // each block. 453a25f0a04SGreg Roach 454a25f0a04SGreg Roach $file_data = ''; 455a25f0a04SGreg Roach 456b7e60af1SGreg Roach $this->deleteGenealogyData((bool) $this->getPreference('keep_media')); 457a25f0a04SGreg Roach $this->setPreference('gedcom_filename', $filename); 458a25f0a04SGreg Roach $this->setPreference('imported', '0'); 459a25f0a04SGreg Roach 4606ccdf4f0SGreg Roach while (!$stream->eof()) { 4616ccdf4f0SGreg Roach $file_data .= $stream->read(65536); 462a25f0a04SGreg Roach // There is no strrpos() function that searches for substrings :-( 463a25f0a04SGreg Roach for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) { 464a25f0a04SGreg Roach if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) { 465a25f0a04SGreg Roach // We’ve found the last record boundary in this chunk of data 466a25f0a04SGreg Roach break; 467a25f0a04SGreg Roach } 468a25f0a04SGreg Roach } 469a25f0a04SGreg Roach if ($pos) { 4701ad2dde6SGreg Roach DB::table('gedcom_chunk')->insert([ 4711ad2dde6SGreg Roach 'gedcom_id' => $this->id, 4721ad2dde6SGreg Roach 'chunk_data' => substr($file_data, 0, $pos), 473c1010edaSGreg Roach ]); 4741ad2dde6SGreg Roach 475a25f0a04SGreg Roach $file_data = substr($file_data, $pos); 476a25f0a04SGreg Roach } 477a25f0a04SGreg Roach } 4781ad2dde6SGreg Roach DB::table('gedcom_chunk')->insert([ 4791ad2dde6SGreg Roach 'gedcom_id' => $this->id, 4801ad2dde6SGreg Roach 'chunk_data' => $file_data, 481c1010edaSGreg Roach ]); 482a25f0a04SGreg Roach 4836ccdf4f0SGreg Roach $stream->close(); 4846ccdf4f0SGreg Roach } 4856ccdf4f0SGreg Roach 4866ccdf4f0SGreg Roach /** 4876ccdf4f0SGreg Roach * Create a new record from GEDCOM data. 4886ccdf4f0SGreg Roach * 4896ccdf4f0SGreg Roach * @param string $gedcom 4906ccdf4f0SGreg Roach * 4916ccdf4f0SGreg Roach * @return GedcomRecord|Individual|Family|Note|Source|Repository|Media 4926ccdf4f0SGreg Roach * @throws InvalidArgumentException 4936ccdf4f0SGreg Roach */ 4946ccdf4f0SGreg Roach public function createRecord(string $gedcom): GedcomRecord 4956ccdf4f0SGreg Roach { 4966ccdf4f0SGreg Roach if (!Str::startsWith($gedcom, '0 @@ ')) { 4976ccdf4f0SGreg Roach throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@'); 4986ccdf4f0SGreg Roach } 4996ccdf4f0SGreg Roach 5006ccdf4f0SGreg Roach $xref = $this->getNewXref(); 5016ccdf4f0SGreg Roach $gedcom = '0 @' . $xref . '@ ' . Str::after($gedcom, '0 @@ '); 5026ccdf4f0SGreg Roach 5036ccdf4f0SGreg Roach // Create a change record 5046ccdf4f0SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 5056ccdf4f0SGreg Roach 5066ccdf4f0SGreg Roach // Create a pending change 5076ccdf4f0SGreg Roach DB::table('change')->insert([ 5086ccdf4f0SGreg Roach 'gedcom_id' => $this->id, 5096ccdf4f0SGreg Roach 'xref' => $xref, 5106ccdf4f0SGreg Roach 'old_gedcom' => '', 5116ccdf4f0SGreg Roach 'new_gedcom' => $gedcom, 5126ccdf4f0SGreg Roach 'user_id' => Auth::id(), 5136ccdf4f0SGreg Roach ]); 5146ccdf4f0SGreg Roach 5156ccdf4f0SGreg Roach // Accept this pending change 516*7c4add84SGreg Roach if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS)) { 51722e73debSGreg Roach $record = new GedcomRecord($xref, $gedcom, null, $this); 5186ccdf4f0SGreg Roach 51922e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 52022e73debSGreg Roach 52122e73debSGreg Roach return $record; 5226ccdf4f0SGreg Roach } 5236ccdf4f0SGreg Roach 5246ccdf4f0SGreg Roach return GedcomRecord::getInstance($xref, $this, $gedcom); 525a25f0a04SGreg Roach } 526304f20d5SGreg Roach 527304f20d5SGreg Roach /** 528b90d8accSGreg Roach * Generate a new XREF, unique across all family trees 529b90d8accSGreg Roach * 530b90d8accSGreg Roach * @return string 531b90d8accSGreg Roach */ 532771ae10aSGreg Roach public function getNewXref(): string 533c1010edaSGreg Roach { 534963fbaeeSGreg Roach // Lock the row, so that only one new XREF may be generated at a time. 535963fbaeeSGreg Roach DB::table('site_setting') 536963fbaeeSGreg Roach ->where('setting_name', '=', 'next_xref') 537963fbaeeSGreg Roach ->lockForUpdate() 538963fbaeeSGreg Roach ->get(); 539963fbaeeSGreg Roach 540a214e186SGreg Roach $prefix = 'X'; 541b90d8accSGreg Roach 542971d66c8SGreg Roach $increment = 1.0; 543b90d8accSGreg Roach do { 544963fbaeeSGreg Roach $num = (int) Site::getPreference('next_xref') + (int) $increment; 545971d66c8SGreg Roach 546971d66c8SGreg Roach // This exponential increment allows us to scan over large blocks of 547971d66c8SGreg Roach // existing data in a reasonable time. 548971d66c8SGreg Roach $increment *= 1.01; 549963fbaeeSGreg Roach 550963fbaeeSGreg Roach $xref = $prefix . $num; 551963fbaeeSGreg Roach 552963fbaeeSGreg Roach // Records may already exist with this sequence number. 553963fbaeeSGreg Roach $already_used = 554963fbaeeSGreg Roach DB::table('individuals')->where('i_id', '=', $xref)->exists() || 555963fbaeeSGreg Roach DB::table('families')->where('f_id', '=', $xref)->exists() || 556963fbaeeSGreg Roach DB::table('sources')->where('s_id', '=', $xref)->exists() || 557963fbaeeSGreg Roach DB::table('media')->where('m_id', '=', $xref)->exists() || 558963fbaeeSGreg Roach DB::table('other')->where('o_id', '=', $xref)->exists() || 559963fbaeeSGreg Roach DB::table('change')->where('xref', '=', $xref)->exists(); 560963fbaeeSGreg Roach } while ($already_used); 561963fbaeeSGreg Roach 562963fbaeeSGreg Roach Site::setPreference('next_xref', (string) $num); 563b90d8accSGreg Roach 564a214e186SGreg Roach return $xref; 565b90d8accSGreg Roach } 566b90d8accSGreg Roach 567b90d8accSGreg Roach /** 568afb591d7SGreg Roach * Create a new family from GEDCOM data. 569afb591d7SGreg Roach * 570afb591d7SGreg Roach * @param string $gedcom 571afb591d7SGreg Roach * 572afb591d7SGreg Roach * @return Family 573afb591d7SGreg Roach * @throws InvalidArgumentException 574afb591d7SGreg Roach */ 575afb591d7SGreg Roach public function createFamily(string $gedcom): GedcomRecord 576afb591d7SGreg Roach { 577bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ FAM')) { 578afb591d7SGreg Roach throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM'); 579afb591d7SGreg Roach } 580afb591d7SGreg Roach 581afb591d7SGreg Roach $xref = $this->getNewXref(); 582bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ FAM' . Str::after($gedcom, '0 @@ FAM'); 583afb591d7SGreg Roach 584afb591d7SGreg Roach // Create a change record 585e5a6b4d4SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 586afb591d7SGreg Roach 587afb591d7SGreg Roach // Create a pending change 588963fbaeeSGreg Roach DB::table('change')->insert([ 589963fbaeeSGreg Roach 'gedcom_id' => $this->id, 590963fbaeeSGreg Roach 'xref' => $xref, 591963fbaeeSGreg Roach 'old_gedcom' => '', 592963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 593963fbaeeSGreg Roach 'user_id' => Auth::id(), 594afb591d7SGreg Roach ]); 595304f20d5SGreg Roach 596304f20d5SGreg Roach // Accept this pending change 597*7c4add84SGreg Roach if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') { 59822e73debSGreg Roach $record = new Family($xref, $gedcom, null, $this); 599afb591d7SGreg Roach 60022e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 60122e73debSGreg Roach 60222e73debSGreg Roach return $record; 603304f20d5SGreg Roach } 604afb591d7SGreg Roach 605afb591d7SGreg Roach return new Family($xref, '', $gedcom, $this); 606afb591d7SGreg Roach } 607afb591d7SGreg Roach 608afb591d7SGreg Roach /** 609afb591d7SGreg Roach * Create a new individual from GEDCOM data. 610afb591d7SGreg Roach * 611afb591d7SGreg Roach * @param string $gedcom 612afb591d7SGreg Roach * 613afb591d7SGreg Roach * @return Individual 614afb591d7SGreg Roach * @throws InvalidArgumentException 615afb591d7SGreg Roach */ 616afb591d7SGreg Roach public function createIndividual(string $gedcom): GedcomRecord 617afb591d7SGreg Roach { 618bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ INDI')) { 619afb591d7SGreg Roach throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI'); 620afb591d7SGreg Roach } 621afb591d7SGreg Roach 622afb591d7SGreg Roach $xref = $this->getNewXref(); 623bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ INDI' . Str::after($gedcom, '0 @@ INDI'); 624afb591d7SGreg Roach 625afb591d7SGreg Roach // Create a change record 626e5a6b4d4SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 627afb591d7SGreg Roach 628afb591d7SGreg Roach // Create a pending change 629963fbaeeSGreg Roach DB::table('change')->insert([ 630963fbaeeSGreg Roach 'gedcom_id' => $this->id, 631963fbaeeSGreg Roach 'xref' => $xref, 632963fbaeeSGreg Roach 'old_gedcom' => '', 633963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 634963fbaeeSGreg Roach 'user_id' => Auth::id(), 635afb591d7SGreg Roach ]); 636afb591d7SGreg Roach 637afb591d7SGreg Roach // Accept this pending change 638*7c4add84SGreg Roach if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') { 63922e73debSGreg Roach $record = new Individual($xref, $gedcom, null, $this); 640afb591d7SGreg Roach 64122e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 64222e73debSGreg Roach 64322e73debSGreg Roach return $record; 644afb591d7SGreg Roach } 645afb591d7SGreg Roach 646afb591d7SGreg Roach return new Individual($xref, '', $gedcom, $this); 647304f20d5SGreg Roach } 6488586983fSGreg Roach 6498586983fSGreg Roach /** 65020b58d20SGreg Roach * Create a new media object from GEDCOM data. 65120b58d20SGreg Roach * 65220b58d20SGreg Roach * @param string $gedcom 65320b58d20SGreg Roach * 65420b58d20SGreg Roach * @return Media 65520b58d20SGreg Roach * @throws InvalidArgumentException 65620b58d20SGreg Roach */ 65720b58d20SGreg Roach public function createMediaObject(string $gedcom): Media 65820b58d20SGreg Roach { 659bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ OBJE')) { 66020b58d20SGreg Roach throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE'); 66120b58d20SGreg Roach } 66220b58d20SGreg Roach 66320b58d20SGreg Roach $xref = $this->getNewXref(); 664bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ OBJE' . Str::after($gedcom, '0 @@ OBJE'); 66520b58d20SGreg Roach 66620b58d20SGreg Roach // Create a change record 667e5a6b4d4SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 66820b58d20SGreg Roach 66920b58d20SGreg Roach // Create a pending change 670963fbaeeSGreg Roach DB::table('change')->insert([ 671963fbaeeSGreg Roach 'gedcom_id' => $this->id, 672963fbaeeSGreg Roach 'xref' => $xref, 673963fbaeeSGreg Roach 'old_gedcom' => '', 674963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 675963fbaeeSGreg Roach 'user_id' => Auth::id(), 67620b58d20SGreg Roach ]); 67720b58d20SGreg Roach 67820b58d20SGreg Roach // Accept this pending change 679*7c4add84SGreg Roach if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') { 68022e73debSGreg Roach $record = new Media($xref, $gedcom, null, $this); 68120b58d20SGreg Roach 68222e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 68322e73debSGreg Roach 68422e73debSGreg Roach return $record; 68520b58d20SGreg Roach } 68620b58d20SGreg Roach 68720b58d20SGreg Roach return new Media($xref, '', $gedcom, $this); 68820b58d20SGreg Roach } 68920b58d20SGreg Roach 69020b58d20SGreg Roach /** 6918586983fSGreg Roach * What is the most significant individual in this tree. 6928586983fSGreg Roach * 693e5a6b4d4SGreg Roach * @param UserInterface $user 6948586983fSGreg Roach * 6958586983fSGreg Roach * @return Individual 6968586983fSGreg Roach */ 697e5a6b4d4SGreg Roach public function significantIndividual(UserInterface $user): Individual 698c1010edaSGreg Roach { 6998f9b0fb2SGreg Roach $individual = null; 7008586983fSGreg Roach 701*7c4add84SGreg Roach if ($this->getUserPreference($user, User::PREF_TREE_DEFAULT_XREF) !== '') { 702*7c4add84SGreg Roach $individual = Individual::getInstance($this->getUserPreference($user, User::PREF_TREE_DEFAULT_XREF), $this); 7038586983fSGreg Roach } 7048f9b0fb2SGreg Roach 705*7c4add84SGreg Roach if ($individual === null && $this->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF) !== '') { 706*7c4add84SGreg Roach $individual = Individual::getInstance($this->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF), $this); 7078586983fSGreg Roach } 7088f9b0fb2SGreg Roach 709bec87e94SGreg Roach if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') { 7108586983fSGreg Roach $individual = Individual::getInstance($this->getPreference('PEDIGREE_ROOT_ID'), $this); 7118586983fSGreg Roach } 7128f9b0fb2SGreg Roach if ($individual === null) { 7138f9b0fb2SGreg Roach $xref = (string) DB::table('individuals') 7148f9b0fb2SGreg Roach ->where('i_file', '=', $this->id()) 7158f9b0fb2SGreg Roach ->min('i_id'); 716769d7d6eSGreg Roach 717769d7d6eSGreg Roach $individual = Individual::getInstance($xref, $this); 7185fe1add5SGreg Roach } 7198f9b0fb2SGreg Roach if ($individual === null) { 7205fe1add5SGreg Roach // always return a record 7215fe1add5SGreg Roach $individual = new Individual('I', '0 @I@ INDI', null, $this); 7225fe1add5SGreg Roach } 7235fe1add5SGreg Roach 7245fe1add5SGreg Roach return $individual; 7255fe1add5SGreg Roach } 7261df7ae39SGreg Roach 72785a166d8SGreg Roach /** 72885a166d8SGreg Roach * Where do we store our media files. 72985a166d8SGreg Roach * 730a04bb9a2SGreg Roach * @param FilesystemInterface $data_filesystem 731a04bb9a2SGreg Roach * 73285a166d8SGreg Roach * @return FilesystemInterface 73385a166d8SGreg Roach */ 734a04bb9a2SGreg Roach public function mediaFilesystem(FilesystemInterface $data_filesystem): FilesystemInterface 7351df7ae39SGreg Roach { 736456d0d35SGreg Roach $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/'); 737a04bb9a2SGreg Roach $adapter = new ChrootAdapter($data_filesystem, $media_dir); 738456d0d35SGreg Roach 739456d0d35SGreg Roach return new Filesystem($adapter); 7401df7ae39SGreg Roach } 741a25f0a04SGreg Roach} 742