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; 263d7a8a4cSGreg Roachuse Fisharebest\Webtrees\Functions\FunctionsImport; 27*22e73debSGreg Roachuse Fisharebest\Webtrees\Services\PendingChangesService; 285afbc57aSGreg Roachuse Fisharebest\Webtrees\Services\TreeService; 2901461f86SGreg Roachuse Illuminate\Database\Capsule\Manager as DB; 30a69f5655SGreg Roachuse Illuminate\Database\Query\Expression; 3194026f20SGreg Roachuse Illuminate\Support\Collection; 32bec87e94SGreg Roachuse Illuminate\Support\Str; 33afb591d7SGreg Roachuse InvalidArgumentException; 341df7ae39SGreg Roachuse League\Flysystem\Filesystem; 351df7ae39SGreg Roachuse League\Flysystem\FilesystemInterface; 366ccdf4f0SGreg Roachuse Psr\Http\Message\StreamInterface; 378b67c11aSGreg Roachuse stdClass; 38a25f0a04SGreg Roach 391e653452SGreg Roachuse function app; 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 * Find the tree with a specific ID. 128a25f0a04SGreg Roach * 1296ccdf4f0SGreg Roach * @param int $tree_id 1306ccdf4f0SGreg Roach * 1316ccdf4f0SGreg Roach * @return Tree 1321e653452SGreg Roach * @deprecated 133a25f0a04SGreg Roach */ 1346ccdf4f0SGreg Roach public static function findById(int $tree_id): Tree 135c1010edaSGreg Roach { 1361e653452SGreg Roach return app(TreeService::class)->all()->first(static function (Tree $tree) use ($tree_id): bool { 1371e653452SGreg Roach return $tree->id() === $tree_id; 1381e653452SGreg Roach }); 1396ccdf4f0SGreg Roach } 1406ccdf4f0SGreg Roach 1416ccdf4f0SGreg Roach /** 1426ccdf4f0SGreg Roach * Set the tree’s configuration settings. 1436ccdf4f0SGreg Roach * 1446ccdf4f0SGreg Roach * @param string $setting_name 1456ccdf4f0SGreg Roach * @param string $setting_value 1466ccdf4f0SGreg Roach * 1476ccdf4f0SGreg Roach * @return $this 1486ccdf4f0SGreg Roach */ 1496ccdf4f0SGreg Roach public function setPreference(string $setting_name, string $setting_value): Tree 1506ccdf4f0SGreg Roach { 1516ccdf4f0SGreg Roach if ($setting_value !== $this->getPreference($setting_name)) { 1526ccdf4f0SGreg Roach DB::table('gedcom_setting')->updateOrInsert([ 1536ccdf4f0SGreg Roach 'gedcom_id' => $this->id, 1546ccdf4f0SGreg Roach 'setting_name' => $setting_name, 1556ccdf4f0SGreg Roach ], [ 1566ccdf4f0SGreg Roach 'setting_value' => $setting_value, 1576ccdf4f0SGreg Roach ]); 1586ccdf4f0SGreg Roach 1596ccdf4f0SGreg Roach $this->preferences[$setting_name] = $setting_value; 1606ccdf4f0SGreg Roach 1616ccdf4f0SGreg Roach Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this); 1626ccdf4f0SGreg Roach } 1636ccdf4f0SGreg Roach 1646ccdf4f0SGreg Roach return $this; 1656ccdf4f0SGreg Roach } 1666ccdf4f0SGreg Roach 1676ccdf4f0SGreg Roach /** 1686ccdf4f0SGreg Roach * Get the tree’s configuration settings. 1696ccdf4f0SGreg Roach * 1706ccdf4f0SGreg Roach * @param string $setting_name 1716ccdf4f0SGreg Roach * @param string $default 1726ccdf4f0SGreg Roach * 1736ccdf4f0SGreg Roach * @return string 1746ccdf4f0SGreg Roach */ 1756ccdf4f0SGreg Roach public function getPreference(string $setting_name, string $default = ''): string 1766ccdf4f0SGreg Roach { 17754c1ab5eSGreg Roach if ($this->preferences === []) { 1786ccdf4f0SGreg Roach $this->preferences = DB::table('gedcom_setting') 1796ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 1806ccdf4f0SGreg Roach ->pluck('setting_value', 'setting_name') 1816ccdf4f0SGreg Roach ->all(); 1826ccdf4f0SGreg Roach } 1836ccdf4f0SGreg Roach 1846ccdf4f0SGreg Roach return $this->preferences[$setting_name] ?? $default; 1856ccdf4f0SGreg Roach } 1866ccdf4f0SGreg Roach 1876ccdf4f0SGreg Roach /** 1886ccdf4f0SGreg Roach * The name of this tree 1896ccdf4f0SGreg Roach * 1906ccdf4f0SGreg Roach * @return string 1916ccdf4f0SGreg Roach */ 1926ccdf4f0SGreg Roach public function name(): string 1936ccdf4f0SGreg Roach { 1946ccdf4f0SGreg Roach return $this->name; 1956ccdf4f0SGreg Roach } 1966ccdf4f0SGreg Roach 1976ccdf4f0SGreg Roach /** 1986ccdf4f0SGreg Roach * The title of this tree 1996ccdf4f0SGreg Roach * 2006ccdf4f0SGreg Roach * @return string 2016ccdf4f0SGreg Roach */ 2026ccdf4f0SGreg Roach public function title(): string 2036ccdf4f0SGreg Roach { 2046ccdf4f0SGreg Roach return $this->title; 2056ccdf4f0SGreg Roach } 2066ccdf4f0SGreg Roach 2076ccdf4f0SGreg Roach /** 2086ccdf4f0SGreg Roach * The fact-level privacy for this tree. 2096ccdf4f0SGreg Roach * 2106ccdf4f0SGreg Roach * @return int[] 2116ccdf4f0SGreg Roach */ 2126ccdf4f0SGreg Roach public function getFactPrivacy(): array 2136ccdf4f0SGreg Roach { 2146ccdf4f0SGreg Roach return $this->fact_privacy; 2156ccdf4f0SGreg Roach } 2166ccdf4f0SGreg Roach 2176ccdf4f0SGreg Roach /** 2186ccdf4f0SGreg Roach * The individual-level privacy for this tree. 2196ccdf4f0SGreg Roach * 2206ccdf4f0SGreg Roach * @return int[] 2216ccdf4f0SGreg Roach */ 2226ccdf4f0SGreg Roach public function getIndividualPrivacy(): array 2236ccdf4f0SGreg Roach { 2246ccdf4f0SGreg Roach return $this->individual_privacy; 2256ccdf4f0SGreg Roach } 2266ccdf4f0SGreg Roach 2276ccdf4f0SGreg Roach /** 2286ccdf4f0SGreg Roach * The individual-fact-level privacy for this tree. 2296ccdf4f0SGreg Roach * 2306ccdf4f0SGreg Roach * @return int[][] 2316ccdf4f0SGreg Roach */ 2326ccdf4f0SGreg Roach public function getIndividualFactPrivacy(): array 2336ccdf4f0SGreg Roach { 2346ccdf4f0SGreg Roach return $this->individual_fact_privacy; 2356ccdf4f0SGreg Roach } 2366ccdf4f0SGreg Roach 2376ccdf4f0SGreg Roach /** 2386ccdf4f0SGreg Roach * Set the tree’s user-configuration settings. 2396ccdf4f0SGreg Roach * 2406ccdf4f0SGreg Roach * @param UserInterface $user 2416ccdf4f0SGreg Roach * @param string $setting_name 2426ccdf4f0SGreg Roach * @param string $setting_value 2436ccdf4f0SGreg Roach * 2446ccdf4f0SGreg Roach * @return $this 2456ccdf4f0SGreg Roach */ 2466ccdf4f0SGreg Roach public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree 2476ccdf4f0SGreg Roach { 2486ccdf4f0SGreg Roach if ($this->getUserPreference($user, $setting_name) !== $setting_value) { 2496ccdf4f0SGreg Roach // Update the database 2506ccdf4f0SGreg Roach DB::table('user_gedcom_setting')->updateOrInsert([ 2516ccdf4f0SGreg Roach 'gedcom_id' => $this->id(), 2526ccdf4f0SGreg Roach 'user_id' => $user->id(), 2536ccdf4f0SGreg Roach 'setting_name' => $setting_name, 2546ccdf4f0SGreg Roach ], [ 2556ccdf4f0SGreg Roach 'setting_value' => $setting_value, 2566ccdf4f0SGreg Roach ]); 2576ccdf4f0SGreg Roach 2586ccdf4f0SGreg Roach // Update the cache 2596ccdf4f0SGreg Roach $this->user_preferences[$user->id()][$setting_name] = $setting_value; 2606ccdf4f0SGreg Roach // Audit log of changes 2616ccdf4f0SGreg Roach Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this); 2626ccdf4f0SGreg Roach } 2636ccdf4f0SGreg Roach 2646ccdf4f0SGreg Roach return $this; 2656ccdf4f0SGreg Roach } 2666ccdf4f0SGreg Roach 2676ccdf4f0SGreg Roach /** 2686ccdf4f0SGreg Roach * Get the tree’s user-configuration settings. 2696ccdf4f0SGreg Roach * 2706ccdf4f0SGreg Roach * @param UserInterface $user 2716ccdf4f0SGreg Roach * @param string $setting_name 2726ccdf4f0SGreg Roach * @param string $default 2736ccdf4f0SGreg Roach * 2746ccdf4f0SGreg Roach * @return string 2756ccdf4f0SGreg Roach */ 2766ccdf4f0SGreg Roach public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string 2776ccdf4f0SGreg Roach { 2786ccdf4f0SGreg Roach // There are lots of settings, and we need to fetch lots of them on every page 2796ccdf4f0SGreg Roach // so it is quicker to fetch them all in one go. 2806ccdf4f0SGreg Roach if (!array_key_exists($user->id(), $this->user_preferences)) { 2816ccdf4f0SGreg Roach $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting') 2826ccdf4f0SGreg Roach ->where('user_id', '=', $user->id()) 2836ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 2846ccdf4f0SGreg Roach ->pluck('setting_value', 'setting_name') 2856ccdf4f0SGreg Roach ->all(); 2866ccdf4f0SGreg Roach } 2876ccdf4f0SGreg Roach 2886ccdf4f0SGreg Roach return $this->user_preferences[$user->id()][$setting_name] ?? $default; 2896ccdf4f0SGreg Roach } 2906ccdf4f0SGreg Roach 2916ccdf4f0SGreg Roach /** 2926ccdf4f0SGreg Roach * The ID of this tree 2936ccdf4f0SGreg Roach * 2946ccdf4f0SGreg Roach * @return int 2956ccdf4f0SGreg Roach */ 2966ccdf4f0SGreg Roach public function id(): int 2976ccdf4f0SGreg Roach { 2986ccdf4f0SGreg Roach return $this->id; 2996ccdf4f0SGreg Roach } 3006ccdf4f0SGreg Roach 3016ccdf4f0SGreg Roach /** 3026ccdf4f0SGreg Roach * Can a user accept changes for this tree? 3036ccdf4f0SGreg Roach * 3046ccdf4f0SGreg Roach * @param UserInterface $user 3056ccdf4f0SGreg Roach * 3066ccdf4f0SGreg Roach * @return bool 3076ccdf4f0SGreg Roach */ 3086ccdf4f0SGreg Roach public function canAcceptChanges(UserInterface $user): bool 3096ccdf4f0SGreg Roach { 3106ccdf4f0SGreg Roach return Auth::isModerator($this, $user); 3116ccdf4f0SGreg Roach } 3126ccdf4f0SGreg Roach 3136ccdf4f0SGreg Roach /** 314b78374c5SGreg Roach * Are there any pending edits for this tree, than need reviewing by a moderator. 315b78374c5SGreg Roach * 316b78374c5SGreg Roach * @return bool 317b78374c5SGreg Roach */ 318771ae10aSGreg Roach public function hasPendingEdit(): bool 319c1010edaSGreg Roach { 32015a3f100SGreg Roach return DB::table('change') 32115a3f100SGreg Roach ->where('gedcom_id', '=', $this->id) 32215a3f100SGreg Roach ->where('status', '=', 'pending') 32315a3f100SGreg Roach ->exists(); 324b78374c5SGreg Roach } 325b78374c5SGreg Roach 326b78374c5SGreg Roach /** 3276ccdf4f0SGreg Roach * Delete everything relating to a tree 3286ccdf4f0SGreg Roach * 3296ccdf4f0SGreg Roach * @return void 3306ccdf4f0SGreg Roach */ 3316ccdf4f0SGreg Roach public function delete(): void 3326ccdf4f0SGreg Roach { 3336ccdf4f0SGreg Roach // If this is the default tree, then unset it 3346ccdf4f0SGreg Roach if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) { 3356ccdf4f0SGreg Roach Site::setPreference('DEFAULT_GEDCOM', ''); 3366ccdf4f0SGreg Roach } 3376ccdf4f0SGreg Roach 3386ccdf4f0SGreg Roach $this->deleteGenealogyData(false); 3396ccdf4f0SGreg Roach 3406ccdf4f0SGreg Roach DB::table('block_setting') 3416ccdf4f0SGreg Roach ->join('block', 'block.block_id', '=', 'block_setting.block_id') 3426ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 3436ccdf4f0SGreg Roach ->delete(); 3446ccdf4f0SGreg Roach DB::table('block')->where('gedcom_id', '=', $this->id)->delete(); 3456ccdf4f0SGreg Roach DB::table('user_gedcom_setting')->where('gedcom_id', '=', $this->id)->delete(); 3466ccdf4f0SGreg Roach DB::table('gedcom_setting')->where('gedcom_id', '=', $this->id)->delete(); 3476ccdf4f0SGreg Roach DB::table('module_privacy')->where('gedcom_id', '=', $this->id)->delete(); 3486ccdf4f0SGreg Roach DB::table('hit_counter')->where('gedcom_id', '=', $this->id)->delete(); 3496ccdf4f0SGreg Roach DB::table('default_resn')->where('gedcom_id', '=', $this->id)->delete(); 3506ccdf4f0SGreg Roach DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete(); 3516ccdf4f0SGreg Roach DB::table('log')->where('gedcom_id', '=', $this->id)->delete(); 3526ccdf4f0SGreg Roach DB::table('gedcom')->where('gedcom_id', '=', $this->id)->delete(); 3536ccdf4f0SGreg Roach } 3546ccdf4f0SGreg Roach 3556ccdf4f0SGreg Roach /** 356a25f0a04SGreg Roach * Delete all the genealogy data from a tree - in preparation for importing 357a25f0a04SGreg Roach * new data. Optionally retain the media data, for when the user has been 358a25f0a04SGreg Roach * editing their data offline using an application which deletes (or does not 359a25f0a04SGreg Roach * support) media data. 360a25f0a04SGreg Roach * 361a25f0a04SGreg Roach * @param bool $keep_media 362b7e60af1SGreg Roach * 363b7e60af1SGreg Roach * @return void 364a25f0a04SGreg Roach */ 365e364afe4SGreg Roach public function deleteGenealogyData(bool $keep_media): void 366c1010edaSGreg Roach { 3671ad2dde6SGreg Roach DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete(); 3681ad2dde6SGreg Roach DB::table('individuals')->where('i_file', '=', $this->id)->delete(); 3691ad2dde6SGreg Roach DB::table('families')->where('f_file', '=', $this->id)->delete(); 3701ad2dde6SGreg Roach DB::table('sources')->where('s_file', '=', $this->id)->delete(); 3711ad2dde6SGreg Roach DB::table('other')->where('o_file', '=', $this->id)->delete(); 3721ad2dde6SGreg Roach DB::table('places')->where('p_file', '=', $this->id)->delete(); 3731ad2dde6SGreg Roach DB::table('placelinks')->where('pl_file', '=', $this->id)->delete(); 3741ad2dde6SGreg Roach DB::table('name')->where('n_file', '=', $this->id)->delete(); 3751ad2dde6SGreg Roach DB::table('dates')->where('d_file', '=', $this->id)->delete(); 3761ad2dde6SGreg Roach DB::table('change')->where('gedcom_id', '=', $this->id)->delete(); 377a25f0a04SGreg Roach 378a25f0a04SGreg Roach if ($keep_media) { 3791ad2dde6SGreg Roach DB::table('link')->where('l_file', '=', $this->id) 3801ad2dde6SGreg Roach ->where('l_type', '<>', 'OBJE') 3811ad2dde6SGreg Roach ->delete(); 382a25f0a04SGreg Roach } else { 3831ad2dde6SGreg Roach DB::table('link')->where('l_file', '=', $this->id)->delete(); 3841ad2dde6SGreg Roach DB::table('media_file')->where('m_file', '=', $this->id)->delete(); 3851ad2dde6SGreg Roach DB::table('media')->where('m_file', '=', $this->id)->delete(); 386a25f0a04SGreg Roach } 387a25f0a04SGreg Roach } 388a25f0a04SGreg Roach 389a25f0a04SGreg Roach /** 390a25f0a04SGreg Roach * Export the tree to a GEDCOM file 391a25f0a04SGreg Roach * 3925792757eSGreg Roach * @param resource $stream 393b7e60af1SGreg Roach * 394b7e60af1SGreg Roach * @return void 395a25f0a04SGreg Roach */ 396425af8b9SGreg Roach public function exportGedcom($stream): void 397c1010edaSGreg Roach { 398a3d8780cSGreg Roach $buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this, 'UTF-8')); 39994026f20SGreg Roach 40094026f20SGreg Roach $union_families = DB::table('families') 40194026f20SGreg Roach ->where('f_file', '=', $this->id) 402a69f5655SGreg Roach ->select(['f_gedcom AS gedcom', 'f_id AS xref', new Expression('LENGTH(f_id) AS len'), new Expression('2 AS n')]); 40394026f20SGreg Roach 40494026f20SGreg Roach $union_sources = DB::table('sources') 40594026f20SGreg Roach ->where('s_file', '=', $this->id) 406a69f5655SGreg Roach ->select(['s_gedcom AS gedcom', 's_id AS xref', new Expression('LENGTH(s_id) AS len'), new Expression('3 AS n')]); 40794026f20SGreg Roach 40894026f20SGreg Roach $union_other = DB::table('other') 40994026f20SGreg Roach ->where('o_file', '=', $this->id) 41094026f20SGreg Roach ->whereNotIn('o_type', ['HEAD', 'TRLR']) 411a69f5655SGreg Roach ->select(['o_gedcom AS gedcom', 'o_id AS xref', new Expression('LENGTH(o_id) AS len'), new Expression('4 AS n')]); 41294026f20SGreg Roach 41394026f20SGreg Roach $union_media = DB::table('media') 41494026f20SGreg Roach ->where('m_file', '=', $this->id) 415a69f5655SGreg Roach ->select(['m_gedcom AS gedcom', 'm_id AS xref', new Expression('LENGTH(m_id) AS len'), new Expression('5 AS n')]); 41694026f20SGreg Roach 417e5a6b4d4SGreg Roach DB::table('individuals') 41894026f20SGreg Roach ->where('i_file', '=', $this->id) 419a69f5655SGreg Roach ->select(['i_gedcom AS gedcom', 'i_id AS xref', new Expression('LENGTH(i_id) AS len'), new Expression('1 AS n')]) 42094026f20SGreg Roach ->union($union_families) 42194026f20SGreg Roach ->union($union_sources) 42294026f20SGreg Roach ->union($union_other) 42394026f20SGreg Roach ->union($union_media) 42494026f20SGreg Roach ->orderBy('n') 42594026f20SGreg Roach ->orderBy('len') 42694026f20SGreg Roach ->orderBy('xref') 42727825e0aSGreg Roach ->chunk(1000, static function (Collection $rows) use ($stream, &$buffer): void { 42894026f20SGreg Roach foreach ($rows as $row) { 4293d7a8a4cSGreg Roach $buffer .= FunctionsExport::reformatRecord($row->gedcom); 430a25f0a04SGreg Roach if (strlen($buffer) > 65535) { 4315792757eSGreg Roach fwrite($stream, $buffer); 432a25f0a04SGreg Roach $buffer = ''; 433a25f0a04SGreg Roach } 434a25f0a04SGreg Roach } 43594026f20SGreg Roach }); 43694026f20SGreg Roach 4370f471f91SGreg Roach fwrite($stream, $buffer . '0 TRLR' . Gedcom::EOL); 438a25f0a04SGreg Roach } 439a25f0a04SGreg Roach 440a25f0a04SGreg Roach /** 441a25f0a04SGreg Roach * Import data from a gedcom file into this tree. 442a25f0a04SGreg Roach * 4436ccdf4f0SGreg Roach * @param StreamInterface $stream The GEDCOM file. 444a25f0a04SGreg Roach * @param string $filename The preferred filename, for export/download. 445a25f0a04SGreg Roach * 446b7e60af1SGreg Roach * @return void 447a25f0a04SGreg Roach */ 4486ccdf4f0SGreg Roach public function importGedcomFile(StreamInterface $stream, string $filename): void 449c1010edaSGreg Roach { 450a25f0a04SGreg Roach // Read the file in blocks of roughly 64K. Ensure that each block 451a25f0a04SGreg Roach // contains complete gedcom records. This will ensure we don’t split 452a25f0a04SGreg Roach // multi-byte characters, as well as simplifying the code to import 453a25f0a04SGreg Roach // each block. 454a25f0a04SGreg Roach 455a25f0a04SGreg Roach $file_data = ''; 456a25f0a04SGreg Roach 457b7e60af1SGreg Roach $this->deleteGenealogyData((bool) $this->getPreference('keep_media')); 458a25f0a04SGreg Roach $this->setPreference('gedcom_filename', $filename); 459a25f0a04SGreg Roach $this->setPreference('imported', '0'); 460a25f0a04SGreg Roach 4616ccdf4f0SGreg Roach while (!$stream->eof()) { 4626ccdf4f0SGreg Roach $file_data .= $stream->read(65536); 463a25f0a04SGreg Roach // There is no strrpos() function that searches for substrings :-( 464a25f0a04SGreg Roach for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) { 465a25f0a04SGreg Roach if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) { 466a25f0a04SGreg Roach // We’ve found the last record boundary in this chunk of data 467a25f0a04SGreg Roach break; 468a25f0a04SGreg Roach } 469a25f0a04SGreg Roach } 470a25f0a04SGreg Roach if ($pos) { 4711ad2dde6SGreg Roach DB::table('gedcom_chunk')->insert([ 4721ad2dde6SGreg Roach 'gedcom_id' => $this->id, 4731ad2dde6SGreg Roach 'chunk_data' => substr($file_data, 0, $pos), 474c1010edaSGreg Roach ]); 4751ad2dde6SGreg Roach 476a25f0a04SGreg Roach $file_data = substr($file_data, $pos); 477a25f0a04SGreg Roach } 478a25f0a04SGreg Roach } 4791ad2dde6SGreg Roach DB::table('gedcom_chunk')->insert([ 4801ad2dde6SGreg Roach 'gedcom_id' => $this->id, 4811ad2dde6SGreg Roach 'chunk_data' => $file_data, 482c1010edaSGreg Roach ]); 483a25f0a04SGreg Roach 4846ccdf4f0SGreg Roach $stream->close(); 4856ccdf4f0SGreg Roach } 4866ccdf4f0SGreg Roach 4876ccdf4f0SGreg Roach /** 4886ccdf4f0SGreg Roach * Create a new record from GEDCOM data. 4896ccdf4f0SGreg Roach * 4906ccdf4f0SGreg Roach * @param string $gedcom 4916ccdf4f0SGreg Roach * 4926ccdf4f0SGreg Roach * @return GedcomRecord|Individual|Family|Note|Source|Repository|Media 4936ccdf4f0SGreg Roach * @throws InvalidArgumentException 4946ccdf4f0SGreg Roach */ 4956ccdf4f0SGreg Roach public function createRecord(string $gedcom): GedcomRecord 4966ccdf4f0SGreg Roach { 4976ccdf4f0SGreg Roach if (!Str::startsWith($gedcom, '0 @@ ')) { 4986ccdf4f0SGreg Roach throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@'); 4996ccdf4f0SGreg Roach } 5006ccdf4f0SGreg Roach 5016ccdf4f0SGreg Roach $xref = $this->getNewXref(); 5026ccdf4f0SGreg Roach $gedcom = '0 @' . $xref . '@ ' . Str::after($gedcom, '0 @@ '); 5036ccdf4f0SGreg Roach 5046ccdf4f0SGreg Roach // Create a change record 5056ccdf4f0SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 5066ccdf4f0SGreg Roach 5076ccdf4f0SGreg Roach // Create a pending change 5086ccdf4f0SGreg Roach DB::table('change')->insert([ 5096ccdf4f0SGreg Roach 'gedcom_id' => $this->id, 5106ccdf4f0SGreg Roach 'xref' => $xref, 5116ccdf4f0SGreg Roach 'old_gedcom' => '', 5126ccdf4f0SGreg Roach 'new_gedcom' => $gedcom, 5136ccdf4f0SGreg Roach 'user_id' => Auth::id(), 5146ccdf4f0SGreg Roach ]); 5156ccdf4f0SGreg Roach 5166ccdf4f0SGreg Roach // Accept this pending change 5176ccdf4f0SGreg Roach if (Auth::user()->getPreference('auto_accept')) { 518*22e73debSGreg Roach $record = new GedcomRecord($xref, $gedcom, null, $this); 5196ccdf4f0SGreg Roach 520*22e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 521*22e73debSGreg Roach 522*22e73debSGreg Roach return $record; 5236ccdf4f0SGreg Roach } 5246ccdf4f0SGreg Roach 5256ccdf4f0SGreg Roach return GedcomRecord::getInstance($xref, $this, $gedcom); 526a25f0a04SGreg Roach } 527304f20d5SGreg Roach 528304f20d5SGreg Roach /** 529b90d8accSGreg Roach * Generate a new XREF, unique across all family trees 530b90d8accSGreg Roach * 531b90d8accSGreg Roach * @return string 532b90d8accSGreg Roach */ 533771ae10aSGreg Roach public function getNewXref(): string 534c1010edaSGreg Roach { 535963fbaeeSGreg Roach // Lock the row, so that only one new XREF may be generated at a time. 536963fbaeeSGreg Roach DB::table('site_setting') 537963fbaeeSGreg Roach ->where('setting_name', '=', 'next_xref') 538963fbaeeSGreg Roach ->lockForUpdate() 539963fbaeeSGreg Roach ->get(); 540963fbaeeSGreg Roach 541a214e186SGreg Roach $prefix = 'X'; 542b90d8accSGreg Roach 543971d66c8SGreg Roach $increment = 1.0; 544b90d8accSGreg Roach do { 545963fbaeeSGreg Roach $num = (int) Site::getPreference('next_xref') + (int) $increment; 546971d66c8SGreg Roach 547971d66c8SGreg Roach // This exponential increment allows us to scan over large blocks of 548971d66c8SGreg Roach // existing data in a reasonable time. 549971d66c8SGreg Roach $increment *= 1.01; 550963fbaeeSGreg Roach 551963fbaeeSGreg Roach $xref = $prefix . $num; 552963fbaeeSGreg Roach 553963fbaeeSGreg Roach // Records may already exist with this sequence number. 554963fbaeeSGreg Roach $already_used = 555963fbaeeSGreg Roach DB::table('individuals')->where('i_id', '=', $xref)->exists() || 556963fbaeeSGreg Roach DB::table('families')->where('f_id', '=', $xref)->exists() || 557963fbaeeSGreg Roach DB::table('sources')->where('s_id', '=', $xref)->exists() || 558963fbaeeSGreg Roach DB::table('media')->where('m_id', '=', $xref)->exists() || 559963fbaeeSGreg Roach DB::table('other')->where('o_id', '=', $xref)->exists() || 560963fbaeeSGreg Roach DB::table('change')->where('xref', '=', $xref)->exists(); 561963fbaeeSGreg Roach } while ($already_used); 562963fbaeeSGreg Roach 563963fbaeeSGreg Roach Site::setPreference('next_xref', (string) $num); 564b90d8accSGreg Roach 565a214e186SGreg Roach return $xref; 566b90d8accSGreg Roach } 567b90d8accSGreg Roach 568b90d8accSGreg Roach /** 569afb591d7SGreg Roach * Create a new family from GEDCOM data. 570afb591d7SGreg Roach * 571afb591d7SGreg Roach * @param string $gedcom 572afb591d7SGreg Roach * 573afb591d7SGreg Roach * @return Family 574afb591d7SGreg Roach * @throws InvalidArgumentException 575afb591d7SGreg Roach */ 576afb591d7SGreg Roach public function createFamily(string $gedcom): GedcomRecord 577afb591d7SGreg Roach { 578bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ FAM')) { 579afb591d7SGreg Roach throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM'); 580afb591d7SGreg Roach } 581afb591d7SGreg Roach 582afb591d7SGreg Roach $xref = $this->getNewXref(); 583bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ FAM' . Str::after($gedcom, '0 @@ FAM'); 584afb591d7SGreg Roach 585afb591d7SGreg Roach // Create a change record 586e5a6b4d4SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 587afb591d7SGreg Roach 588afb591d7SGreg Roach // Create a pending change 589963fbaeeSGreg Roach DB::table('change')->insert([ 590963fbaeeSGreg Roach 'gedcom_id' => $this->id, 591963fbaeeSGreg Roach 'xref' => $xref, 592963fbaeeSGreg Roach 'old_gedcom' => '', 593963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 594963fbaeeSGreg Roach 'user_id' => Auth::id(), 595afb591d7SGreg Roach ]); 596304f20d5SGreg Roach 597304f20d5SGreg Roach // Accept this pending change 598304f20d5SGreg Roach if (Auth::user()->getPreference('auto_accept')) { 599*22e73debSGreg Roach $record = new Family($xref, $gedcom, null, $this); 600afb591d7SGreg Roach 601*22e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 602*22e73debSGreg Roach 603*22e73debSGreg Roach return $record; 604304f20d5SGreg Roach } 605afb591d7SGreg Roach 606afb591d7SGreg Roach return new Family($xref, '', $gedcom, $this); 607afb591d7SGreg Roach } 608afb591d7SGreg Roach 609afb591d7SGreg Roach /** 610afb591d7SGreg Roach * Create a new individual from GEDCOM data. 611afb591d7SGreg Roach * 612afb591d7SGreg Roach * @param string $gedcom 613afb591d7SGreg Roach * 614afb591d7SGreg Roach * @return Individual 615afb591d7SGreg Roach * @throws InvalidArgumentException 616afb591d7SGreg Roach */ 617afb591d7SGreg Roach public function createIndividual(string $gedcom): GedcomRecord 618afb591d7SGreg Roach { 619bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ INDI')) { 620afb591d7SGreg Roach throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI'); 621afb591d7SGreg Roach } 622afb591d7SGreg Roach 623afb591d7SGreg Roach $xref = $this->getNewXref(); 624bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ INDI' . Str::after($gedcom, '0 @@ INDI'); 625afb591d7SGreg Roach 626afb591d7SGreg Roach // Create a change record 627e5a6b4d4SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 628afb591d7SGreg Roach 629afb591d7SGreg 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(), 636afb591d7SGreg Roach ]); 637afb591d7SGreg Roach 638afb591d7SGreg Roach // Accept this pending change 639afb591d7SGreg Roach if (Auth::user()->getPreference('auto_accept')) { 640*22e73debSGreg Roach $record = new Individual($xref, $gedcom, null, $this); 641afb591d7SGreg Roach 642*22e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 643*22e73debSGreg Roach 644*22e73debSGreg Roach return $record; 645afb591d7SGreg Roach } 646afb591d7SGreg Roach 647afb591d7SGreg Roach return new Individual($xref, '', $gedcom, $this); 648304f20d5SGreg Roach } 6498586983fSGreg Roach 6508586983fSGreg Roach /** 65120b58d20SGreg Roach * Create a new media object from GEDCOM data. 65220b58d20SGreg Roach * 65320b58d20SGreg Roach * @param string $gedcom 65420b58d20SGreg Roach * 65520b58d20SGreg Roach * @return Media 65620b58d20SGreg Roach * @throws InvalidArgumentException 65720b58d20SGreg Roach */ 65820b58d20SGreg Roach public function createMediaObject(string $gedcom): Media 65920b58d20SGreg Roach { 660bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ OBJE')) { 66120b58d20SGreg Roach throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE'); 66220b58d20SGreg Roach } 66320b58d20SGreg Roach 66420b58d20SGreg Roach $xref = $this->getNewXref(); 665bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ OBJE' . Str::after($gedcom, '0 @@ OBJE'); 66620b58d20SGreg Roach 66720b58d20SGreg Roach // Create a change record 668e5a6b4d4SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 66920b58d20SGreg Roach 67020b58d20SGreg Roach // Create a pending change 671963fbaeeSGreg Roach DB::table('change')->insert([ 672963fbaeeSGreg Roach 'gedcom_id' => $this->id, 673963fbaeeSGreg Roach 'xref' => $xref, 674963fbaeeSGreg Roach 'old_gedcom' => '', 675963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 676963fbaeeSGreg Roach 'user_id' => Auth::id(), 67720b58d20SGreg Roach ]); 67820b58d20SGreg Roach 67920b58d20SGreg Roach // Accept this pending change 68020b58d20SGreg Roach if (Auth::user()->getPreference('auto_accept')) { 681*22e73debSGreg Roach $record = new Media($xref, $gedcom, null, $this); 68220b58d20SGreg Roach 683*22e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 684*22e73debSGreg Roach 685*22e73debSGreg Roach return $record; 68620b58d20SGreg Roach } 68720b58d20SGreg Roach 68820b58d20SGreg Roach return new Media($xref, '', $gedcom, $this); 68920b58d20SGreg Roach } 69020b58d20SGreg Roach 69120b58d20SGreg Roach /** 6928586983fSGreg Roach * What is the most significant individual in this tree. 6938586983fSGreg Roach * 694e5a6b4d4SGreg Roach * @param UserInterface $user 6958586983fSGreg Roach * 6968586983fSGreg Roach * @return Individual 6978586983fSGreg Roach */ 698e5a6b4d4SGreg Roach public function significantIndividual(UserInterface $user): Individual 699c1010edaSGreg Roach { 7008f9b0fb2SGreg Roach $individual = null; 7018586983fSGreg Roach 7028f9b0fb2SGreg Roach if ($this->getUserPreference($user, 'rootid') !== '') { 7038586983fSGreg Roach $individual = Individual::getInstance($this->getUserPreference($user, 'rootid'), $this); 7048586983fSGreg Roach } 7058f9b0fb2SGreg Roach 7068f9b0fb2SGreg Roach if ($individual === null && $this->getUserPreference($user, 'gedcomid') !== '') { 7078586983fSGreg Roach $individual = Individual::getInstance($this->getUserPreference($user, 'gedcomid'), $this); 7088586983fSGreg Roach } 7098f9b0fb2SGreg Roach 710bec87e94SGreg Roach if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') { 7118586983fSGreg Roach $individual = Individual::getInstance($this->getPreference('PEDIGREE_ROOT_ID'), $this); 7128586983fSGreg Roach } 7138f9b0fb2SGreg Roach if ($individual === null) { 7148f9b0fb2SGreg Roach $xref = (string) DB::table('individuals') 7158f9b0fb2SGreg Roach ->where('i_file', '=', $this->id()) 7168f9b0fb2SGreg Roach ->min('i_id'); 717769d7d6eSGreg Roach 718769d7d6eSGreg Roach $individual = Individual::getInstance($xref, $this); 7195fe1add5SGreg Roach } 7208f9b0fb2SGreg Roach if ($individual === null) { 7215fe1add5SGreg Roach // always return a record 7225fe1add5SGreg Roach $individual = new Individual('I', '0 @I@ INDI', null, $this); 7235fe1add5SGreg Roach } 7245fe1add5SGreg Roach 7255fe1add5SGreg Roach return $individual; 7265fe1add5SGreg Roach } 7271df7ae39SGreg Roach 72885a166d8SGreg Roach /** 72985a166d8SGreg Roach * Where do we store our media files. 73085a166d8SGreg Roach * 73185a166d8SGreg Roach * @return FilesystemInterface 73285a166d8SGreg Roach */ 7331df7ae39SGreg Roach public function mediaFilesystem(): FilesystemInterface 7341df7ae39SGreg Roach { 735456d0d35SGreg Roach $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/'); 736456d0d35SGreg Roach $filesystem = app(FilesystemInterface::class); 737456d0d35SGreg Roach $adapter = new ChrootAdapter($filesystem, $media_dir); 738456d0d35SGreg Roach 739456d0d35SGreg Roach return new Filesystem($adapter); 7401df7ae39SGreg Roach } 741a25f0a04SGreg Roach} 742