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; 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; 381e653452SGreg Roach 39a25f0a04SGreg Roach/** 4076692c8bSGreg Roach * Provide an interface to the wt_gedcom table. 41a25f0a04SGreg Roach */ 42c1010edaSGreg Roachclass Tree 43c1010edaSGreg Roach{ 44061b43d7SGreg Roach private const RESN_PRIVACY = [ 45061b43d7SGreg Roach 'none' => Auth::PRIV_PRIVATE, 46061b43d7SGreg Roach 'privacy' => Auth::PRIV_USER, 47061b43d7SGreg Roach 'confidential' => Auth::PRIV_NONE, 48061b43d7SGreg Roach 'hidden' => Auth::PRIV_HIDE, 49061b43d7SGreg Roach ]; 503df1e584SGreg Roach 516ccdf4f0SGreg Roach /** @var int The tree's ID number */ 526ccdf4f0SGreg Roach private $id; 533df1e584SGreg Roach 546ccdf4f0SGreg Roach /** @var string The tree's name */ 556ccdf4f0SGreg Roach private $name; 563df1e584SGreg Roach 576ccdf4f0SGreg Roach /** @var string The tree's title */ 586ccdf4f0SGreg Roach private $title; 593df1e584SGreg Roach 606ccdf4f0SGreg Roach /** @var int[] Default access rules for facts in this tree */ 616ccdf4f0SGreg Roach private $fact_privacy; 623df1e584SGreg Roach 636ccdf4f0SGreg Roach /** @var int[] Default access rules for individuals in this tree */ 646ccdf4f0SGreg Roach private $individual_privacy; 653df1e584SGreg Roach 666ccdf4f0SGreg Roach /** @var integer[][] Default access rules for individual facts in this tree */ 676ccdf4f0SGreg Roach private $individual_fact_privacy; 683df1e584SGreg Roach 696ccdf4f0SGreg Roach /** @var string[] Cached copy of the wt_gedcom_setting table. */ 706ccdf4f0SGreg Roach private $preferences = []; 713df1e584SGreg Roach 726ccdf4f0SGreg Roach /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */ 736ccdf4f0SGreg Roach private $user_preferences = []; 74061b43d7SGreg Roach 75a25f0a04SGreg Roach /** 763df1e584SGreg Roach * Create a tree object. 77a25f0a04SGreg Roach * 7872cf66d4SGreg Roach * @param int $id 79aa6f03bbSGreg Roach * @param string $name 80cc13d6d8SGreg Roach * @param string $title 81a25f0a04SGreg Roach */ 825afbc57aSGreg Roach public function __construct(int $id, string $name, string $title) 83c1010edaSGreg Roach { 8472cf66d4SGreg Roach $this->id = $id; 85aa6f03bbSGreg Roach $this->name = $name; 86cc13d6d8SGreg Roach $this->title = $title; 8713abd6f3SGreg Roach $this->fact_privacy = []; 8813abd6f3SGreg Roach $this->individual_privacy = []; 8913abd6f3SGreg Roach $this->individual_fact_privacy = []; 90518bbdc1SGreg Roach 91518bbdc1SGreg Roach // Load the privacy settings for this tree 92061b43d7SGreg Roach $rows = DB::table('default_resn') 93061b43d7SGreg Roach ->where('gedcom_id', '=', $this->id) 94061b43d7SGreg Roach ->get(); 95518bbdc1SGreg Roach 96518bbdc1SGreg Roach foreach ($rows as $row) { 97061b43d7SGreg Roach // Convert GEDCOM privacy restriction to a webtrees access level. 98061b43d7SGreg Roach $row->resn = self::RESN_PRIVACY[$row->resn]; 99061b43d7SGreg Roach 100518bbdc1SGreg Roach if ($row->xref !== null) { 101518bbdc1SGreg Roach if ($row->tag_type !== null) { 102b262b3d3SGreg Roach $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn; 103518bbdc1SGreg Roach } else { 104b262b3d3SGreg Roach $this->individual_privacy[$row->xref] = $row->resn; 105518bbdc1SGreg Roach } 106518bbdc1SGreg Roach } else { 107b262b3d3SGreg Roach $this->fact_privacy[$row->tag_type] = $row->resn; 108518bbdc1SGreg Roach } 109518bbdc1SGreg Roach } 110a25f0a04SGreg Roach } 111a25f0a04SGreg Roach 112a25f0a04SGreg Roach /** 1135afbc57aSGreg Roach * A closure which will create a record from a database row. 1145afbc57aSGreg Roach * 1155afbc57aSGreg Roach * @return Closure 1165afbc57aSGreg Roach */ 1175afbc57aSGreg Roach public static function rowMapper(): Closure 1185afbc57aSGreg Roach { 1195afbc57aSGreg Roach return static function (stdClass $row): Tree { 1205afbc57aSGreg Roach return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title); 1215afbc57aSGreg Roach }; 1225afbc57aSGreg Roach } 1235afbc57aSGreg Roach 1245afbc57aSGreg Roach /** 1256ccdf4f0SGreg Roach * Set the tree’s configuration settings. 1266ccdf4f0SGreg Roach * 1276ccdf4f0SGreg Roach * @param string $setting_name 1286ccdf4f0SGreg Roach * @param string $setting_value 1296ccdf4f0SGreg Roach * 1306ccdf4f0SGreg Roach * @return $this 1316ccdf4f0SGreg Roach */ 1326ccdf4f0SGreg Roach public function setPreference(string $setting_name, string $setting_value): Tree 1336ccdf4f0SGreg Roach { 1346ccdf4f0SGreg Roach if ($setting_value !== $this->getPreference($setting_name)) { 1356ccdf4f0SGreg Roach DB::table('gedcom_setting')->updateOrInsert([ 1366ccdf4f0SGreg Roach 'gedcom_id' => $this->id, 1376ccdf4f0SGreg Roach 'setting_name' => $setting_name, 1386ccdf4f0SGreg Roach ], [ 1396ccdf4f0SGreg Roach 'setting_value' => $setting_value, 1406ccdf4f0SGreg Roach ]); 1416ccdf4f0SGreg Roach 1426ccdf4f0SGreg Roach $this->preferences[$setting_name] = $setting_value; 1436ccdf4f0SGreg Roach 1446ccdf4f0SGreg Roach Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this); 1456ccdf4f0SGreg Roach } 1466ccdf4f0SGreg Roach 1476ccdf4f0SGreg Roach return $this; 1486ccdf4f0SGreg Roach } 1496ccdf4f0SGreg Roach 1506ccdf4f0SGreg Roach /** 1516ccdf4f0SGreg Roach * Get the tree’s configuration settings. 1526ccdf4f0SGreg Roach * 1536ccdf4f0SGreg Roach * @param string $setting_name 1546ccdf4f0SGreg Roach * @param string $default 1556ccdf4f0SGreg Roach * 1566ccdf4f0SGreg Roach * @return string 1576ccdf4f0SGreg Roach */ 1586ccdf4f0SGreg Roach public function getPreference(string $setting_name, string $default = ''): string 1596ccdf4f0SGreg Roach { 16054c1ab5eSGreg Roach if ($this->preferences === []) { 1616ccdf4f0SGreg Roach $this->preferences = DB::table('gedcom_setting') 1626ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 1636ccdf4f0SGreg Roach ->pluck('setting_value', 'setting_name') 1646ccdf4f0SGreg Roach ->all(); 1656ccdf4f0SGreg Roach } 1666ccdf4f0SGreg Roach 1676ccdf4f0SGreg Roach return $this->preferences[$setting_name] ?? $default; 1686ccdf4f0SGreg Roach } 1696ccdf4f0SGreg Roach 1706ccdf4f0SGreg Roach /** 1716ccdf4f0SGreg Roach * The name of this tree 1726ccdf4f0SGreg Roach * 1736ccdf4f0SGreg Roach * @return string 1746ccdf4f0SGreg Roach */ 1756ccdf4f0SGreg Roach public function name(): string 1766ccdf4f0SGreg Roach { 1776ccdf4f0SGreg Roach return $this->name; 1786ccdf4f0SGreg Roach } 1796ccdf4f0SGreg Roach 1806ccdf4f0SGreg Roach /** 1816ccdf4f0SGreg Roach * The title of this tree 1826ccdf4f0SGreg Roach * 1836ccdf4f0SGreg Roach * @return string 1846ccdf4f0SGreg Roach */ 1856ccdf4f0SGreg Roach public function title(): string 1866ccdf4f0SGreg Roach { 1876ccdf4f0SGreg Roach return $this->title; 1886ccdf4f0SGreg Roach } 1896ccdf4f0SGreg Roach 1906ccdf4f0SGreg Roach /** 1916ccdf4f0SGreg Roach * The fact-level privacy for this tree. 1926ccdf4f0SGreg Roach * 1936ccdf4f0SGreg Roach * @return int[] 1946ccdf4f0SGreg Roach */ 1956ccdf4f0SGreg Roach public function getFactPrivacy(): array 1966ccdf4f0SGreg Roach { 1976ccdf4f0SGreg Roach return $this->fact_privacy; 1986ccdf4f0SGreg Roach } 1996ccdf4f0SGreg Roach 2006ccdf4f0SGreg Roach /** 2016ccdf4f0SGreg Roach * The individual-level privacy for this tree. 2026ccdf4f0SGreg Roach * 2036ccdf4f0SGreg Roach * @return int[] 2046ccdf4f0SGreg Roach */ 2056ccdf4f0SGreg Roach public function getIndividualPrivacy(): array 2066ccdf4f0SGreg Roach { 2076ccdf4f0SGreg Roach return $this->individual_privacy; 2086ccdf4f0SGreg Roach } 2096ccdf4f0SGreg Roach 2106ccdf4f0SGreg Roach /** 2116ccdf4f0SGreg Roach * The individual-fact-level privacy for this tree. 2126ccdf4f0SGreg Roach * 2136ccdf4f0SGreg Roach * @return int[][] 2146ccdf4f0SGreg Roach */ 2156ccdf4f0SGreg Roach public function getIndividualFactPrivacy(): array 2166ccdf4f0SGreg Roach { 2176ccdf4f0SGreg Roach return $this->individual_fact_privacy; 2186ccdf4f0SGreg Roach } 2196ccdf4f0SGreg Roach 2206ccdf4f0SGreg Roach /** 2216ccdf4f0SGreg Roach * Set the tree’s user-configuration settings. 2226ccdf4f0SGreg Roach * 2236ccdf4f0SGreg Roach * @param UserInterface $user 2246ccdf4f0SGreg Roach * @param string $setting_name 2256ccdf4f0SGreg Roach * @param string $setting_value 2266ccdf4f0SGreg Roach * 2276ccdf4f0SGreg Roach * @return $this 2286ccdf4f0SGreg Roach */ 2296ccdf4f0SGreg Roach public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree 2306ccdf4f0SGreg Roach { 2316ccdf4f0SGreg Roach if ($this->getUserPreference($user, $setting_name) !== $setting_value) { 2326ccdf4f0SGreg Roach // Update the database 2336ccdf4f0SGreg Roach DB::table('user_gedcom_setting')->updateOrInsert([ 2346ccdf4f0SGreg Roach 'gedcom_id' => $this->id(), 2356ccdf4f0SGreg Roach 'user_id' => $user->id(), 2366ccdf4f0SGreg Roach 'setting_name' => $setting_name, 2376ccdf4f0SGreg Roach ], [ 2386ccdf4f0SGreg Roach 'setting_value' => $setting_value, 2396ccdf4f0SGreg Roach ]); 2406ccdf4f0SGreg Roach 2416ccdf4f0SGreg Roach // Update the cache 2426ccdf4f0SGreg Roach $this->user_preferences[$user->id()][$setting_name] = $setting_value; 2436ccdf4f0SGreg Roach // Audit log of changes 2446ccdf4f0SGreg Roach Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this); 2456ccdf4f0SGreg Roach } 2466ccdf4f0SGreg Roach 2476ccdf4f0SGreg Roach return $this; 2486ccdf4f0SGreg Roach } 2496ccdf4f0SGreg Roach 2506ccdf4f0SGreg Roach /** 2516ccdf4f0SGreg Roach * Get the tree’s user-configuration settings. 2526ccdf4f0SGreg Roach * 2536ccdf4f0SGreg Roach * @param UserInterface $user 2546ccdf4f0SGreg Roach * @param string $setting_name 2556ccdf4f0SGreg Roach * @param string $default 2566ccdf4f0SGreg Roach * 2576ccdf4f0SGreg Roach * @return string 2586ccdf4f0SGreg Roach */ 2596ccdf4f0SGreg Roach public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string 2606ccdf4f0SGreg Roach { 2616ccdf4f0SGreg Roach // There are lots of settings, and we need to fetch lots of them on every page 2626ccdf4f0SGreg Roach // so it is quicker to fetch them all in one go. 2636ccdf4f0SGreg Roach if (!array_key_exists($user->id(), $this->user_preferences)) { 2646ccdf4f0SGreg Roach $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting') 2656ccdf4f0SGreg Roach ->where('user_id', '=', $user->id()) 2666ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 2676ccdf4f0SGreg Roach ->pluck('setting_value', 'setting_name') 2686ccdf4f0SGreg Roach ->all(); 2696ccdf4f0SGreg Roach } 2706ccdf4f0SGreg Roach 2716ccdf4f0SGreg Roach return $this->user_preferences[$user->id()][$setting_name] ?? $default; 2726ccdf4f0SGreg Roach } 2736ccdf4f0SGreg Roach 2746ccdf4f0SGreg Roach /** 2756ccdf4f0SGreg Roach * The ID of this tree 2766ccdf4f0SGreg Roach * 2776ccdf4f0SGreg Roach * @return int 2786ccdf4f0SGreg Roach */ 2796ccdf4f0SGreg Roach public function id(): int 2806ccdf4f0SGreg Roach { 2816ccdf4f0SGreg Roach return $this->id; 2826ccdf4f0SGreg Roach } 2836ccdf4f0SGreg Roach 2846ccdf4f0SGreg Roach /** 2856ccdf4f0SGreg Roach * Can a user accept changes for this tree? 2866ccdf4f0SGreg Roach * 2876ccdf4f0SGreg Roach * @param UserInterface $user 2886ccdf4f0SGreg Roach * 2896ccdf4f0SGreg Roach * @return bool 2906ccdf4f0SGreg Roach */ 2916ccdf4f0SGreg Roach public function canAcceptChanges(UserInterface $user): bool 2926ccdf4f0SGreg Roach { 2936ccdf4f0SGreg Roach return Auth::isModerator($this, $user); 2946ccdf4f0SGreg Roach } 2956ccdf4f0SGreg Roach 2966ccdf4f0SGreg Roach /** 297b78374c5SGreg Roach * Are there any pending edits for this tree, than need reviewing by a moderator. 298b78374c5SGreg Roach * 299b78374c5SGreg Roach * @return bool 300b78374c5SGreg Roach */ 301771ae10aSGreg Roach public function hasPendingEdit(): bool 302c1010edaSGreg Roach { 30315a3f100SGreg Roach return DB::table('change') 30415a3f100SGreg Roach ->where('gedcom_id', '=', $this->id) 30515a3f100SGreg Roach ->where('status', '=', 'pending') 30615a3f100SGreg Roach ->exists(); 307b78374c5SGreg Roach } 308b78374c5SGreg Roach 309b78374c5SGreg Roach /** 3106ccdf4f0SGreg Roach * Delete everything relating to a tree 3116ccdf4f0SGreg Roach * 3126ccdf4f0SGreg Roach * @return void 3136ccdf4f0SGreg Roach */ 3146ccdf4f0SGreg Roach public function delete(): void 3156ccdf4f0SGreg Roach { 3166ccdf4f0SGreg Roach // If this is the default tree, then unset it 3176ccdf4f0SGreg Roach if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) { 3186ccdf4f0SGreg Roach Site::setPreference('DEFAULT_GEDCOM', ''); 3196ccdf4f0SGreg Roach } 3206ccdf4f0SGreg Roach 3216ccdf4f0SGreg Roach $this->deleteGenealogyData(false); 3226ccdf4f0SGreg Roach 3236ccdf4f0SGreg Roach DB::table('block_setting') 3246ccdf4f0SGreg Roach ->join('block', 'block.block_id', '=', 'block_setting.block_id') 3256ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 3266ccdf4f0SGreg Roach ->delete(); 3276ccdf4f0SGreg Roach DB::table('block')->where('gedcom_id', '=', $this->id)->delete(); 3286ccdf4f0SGreg Roach DB::table('user_gedcom_setting')->where('gedcom_id', '=', $this->id)->delete(); 3296ccdf4f0SGreg Roach DB::table('gedcom_setting')->where('gedcom_id', '=', $this->id)->delete(); 3306ccdf4f0SGreg Roach DB::table('module_privacy')->where('gedcom_id', '=', $this->id)->delete(); 3316ccdf4f0SGreg Roach DB::table('hit_counter')->where('gedcom_id', '=', $this->id)->delete(); 3326ccdf4f0SGreg Roach DB::table('default_resn')->where('gedcom_id', '=', $this->id)->delete(); 3336ccdf4f0SGreg Roach DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete(); 3346ccdf4f0SGreg Roach DB::table('log')->where('gedcom_id', '=', $this->id)->delete(); 3356ccdf4f0SGreg Roach DB::table('gedcom')->where('gedcom_id', '=', $this->id)->delete(); 3366ccdf4f0SGreg Roach } 3376ccdf4f0SGreg Roach 3386ccdf4f0SGreg Roach /** 339a25f0a04SGreg Roach * Delete all the genealogy data from a tree - in preparation for importing 340a25f0a04SGreg Roach * new data. Optionally retain the media data, for when the user has been 341a25f0a04SGreg Roach * editing their data offline using an application which deletes (or does not 342a25f0a04SGreg Roach * support) media data. 343a25f0a04SGreg Roach * 344a25f0a04SGreg Roach * @param bool $keep_media 345b7e60af1SGreg Roach * 346b7e60af1SGreg Roach * @return void 347a25f0a04SGreg Roach */ 348e364afe4SGreg Roach public function deleteGenealogyData(bool $keep_media): void 349c1010edaSGreg Roach { 3501ad2dde6SGreg Roach DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete(); 3511ad2dde6SGreg Roach DB::table('individuals')->where('i_file', '=', $this->id)->delete(); 3521ad2dde6SGreg Roach DB::table('families')->where('f_file', '=', $this->id)->delete(); 3531ad2dde6SGreg Roach DB::table('sources')->where('s_file', '=', $this->id)->delete(); 3541ad2dde6SGreg Roach DB::table('other')->where('o_file', '=', $this->id)->delete(); 3551ad2dde6SGreg Roach DB::table('places')->where('p_file', '=', $this->id)->delete(); 3561ad2dde6SGreg Roach DB::table('placelinks')->where('pl_file', '=', $this->id)->delete(); 3571ad2dde6SGreg Roach DB::table('name')->where('n_file', '=', $this->id)->delete(); 3581ad2dde6SGreg Roach DB::table('dates')->where('d_file', '=', $this->id)->delete(); 3591ad2dde6SGreg Roach DB::table('change')->where('gedcom_id', '=', $this->id)->delete(); 360a25f0a04SGreg Roach 361a25f0a04SGreg Roach if ($keep_media) { 3621ad2dde6SGreg Roach DB::table('link')->where('l_file', '=', $this->id) 3631ad2dde6SGreg Roach ->where('l_type', '<>', 'OBJE') 3641ad2dde6SGreg Roach ->delete(); 365a25f0a04SGreg Roach } else { 3661ad2dde6SGreg Roach DB::table('link')->where('l_file', '=', $this->id)->delete(); 3671ad2dde6SGreg Roach DB::table('media_file')->where('m_file', '=', $this->id)->delete(); 3681ad2dde6SGreg Roach DB::table('media')->where('m_file', '=', $this->id)->delete(); 369a25f0a04SGreg Roach } 370a25f0a04SGreg Roach } 371a25f0a04SGreg Roach 372a25f0a04SGreg Roach /** 373a25f0a04SGreg Roach * Export the tree to a GEDCOM file 374a25f0a04SGreg Roach * 3755792757eSGreg Roach * @param resource $stream 376b7e60af1SGreg Roach * 377b7e60af1SGreg Roach * @return void 378a25f0a04SGreg Roach */ 379425af8b9SGreg Roach public function exportGedcom($stream): void 380c1010edaSGreg Roach { 381a3d8780cSGreg Roach $buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this, 'UTF-8')); 38294026f20SGreg Roach 38394026f20SGreg Roach $union_families = DB::table('families') 38494026f20SGreg Roach ->where('f_file', '=', $this->id) 385a69f5655SGreg Roach ->select(['f_gedcom AS gedcom', 'f_id AS xref', new Expression('LENGTH(f_id) AS len'), new Expression('2 AS n')]); 38694026f20SGreg Roach 38794026f20SGreg Roach $union_sources = DB::table('sources') 38894026f20SGreg Roach ->where('s_file', '=', $this->id) 389a69f5655SGreg Roach ->select(['s_gedcom AS gedcom', 's_id AS xref', new Expression('LENGTH(s_id) AS len'), new Expression('3 AS n')]); 39094026f20SGreg Roach 39194026f20SGreg Roach $union_other = DB::table('other') 39294026f20SGreg Roach ->where('o_file', '=', $this->id) 39394026f20SGreg Roach ->whereNotIn('o_type', ['HEAD', 'TRLR']) 394a69f5655SGreg Roach ->select(['o_gedcom AS gedcom', 'o_id AS xref', new Expression('LENGTH(o_id) AS len'), new Expression('4 AS n')]); 39594026f20SGreg Roach 39694026f20SGreg Roach $union_media = DB::table('media') 39794026f20SGreg Roach ->where('m_file', '=', $this->id) 398a69f5655SGreg Roach ->select(['m_gedcom AS gedcom', 'm_id AS xref', new Expression('LENGTH(m_id) AS len'), new Expression('5 AS n')]); 39994026f20SGreg Roach 400e5a6b4d4SGreg Roach DB::table('individuals') 40194026f20SGreg Roach ->where('i_file', '=', $this->id) 402a69f5655SGreg Roach ->select(['i_gedcom AS gedcom', 'i_id AS xref', new Expression('LENGTH(i_id) AS len'), new Expression('1 AS n')]) 40394026f20SGreg Roach ->union($union_families) 40494026f20SGreg Roach ->union($union_sources) 40594026f20SGreg Roach ->union($union_other) 40694026f20SGreg Roach ->union($union_media) 40794026f20SGreg Roach ->orderBy('n') 40894026f20SGreg Roach ->orderBy('len') 40994026f20SGreg Roach ->orderBy('xref') 41027825e0aSGreg Roach ->chunk(1000, static function (Collection $rows) use ($stream, &$buffer): void { 41194026f20SGreg Roach foreach ($rows as $row) { 4123d7a8a4cSGreg Roach $buffer .= FunctionsExport::reformatRecord($row->gedcom); 413a25f0a04SGreg Roach if (strlen($buffer) > 65535) { 4145792757eSGreg Roach fwrite($stream, $buffer); 415a25f0a04SGreg Roach $buffer = ''; 416a25f0a04SGreg Roach } 417a25f0a04SGreg Roach } 41894026f20SGreg Roach }); 41994026f20SGreg Roach 4200f471f91SGreg Roach fwrite($stream, $buffer . '0 TRLR' . Gedcom::EOL); 421a25f0a04SGreg Roach } 422a25f0a04SGreg Roach 423a25f0a04SGreg Roach /** 424a25f0a04SGreg Roach * Import data from a gedcom file into this tree. 425a25f0a04SGreg Roach * 4266ccdf4f0SGreg Roach * @param StreamInterface $stream The GEDCOM file. 427a25f0a04SGreg Roach * @param string $filename The preferred filename, for export/download. 428a25f0a04SGreg Roach * 429b7e60af1SGreg Roach * @return void 430a25f0a04SGreg Roach */ 4316ccdf4f0SGreg Roach public function importGedcomFile(StreamInterface $stream, string $filename): void 432c1010edaSGreg Roach { 433a25f0a04SGreg Roach // Read the file in blocks of roughly 64K. Ensure that each block 434a25f0a04SGreg Roach // contains complete gedcom records. This will ensure we don’t split 435a25f0a04SGreg Roach // multi-byte characters, as well as simplifying the code to import 436a25f0a04SGreg Roach // each block. 437a25f0a04SGreg Roach 438a25f0a04SGreg Roach $file_data = ''; 439a25f0a04SGreg Roach 440b7e60af1SGreg Roach $this->deleteGenealogyData((bool) $this->getPreference('keep_media')); 441a25f0a04SGreg Roach $this->setPreference('gedcom_filename', $filename); 442a25f0a04SGreg Roach $this->setPreference('imported', '0'); 443a25f0a04SGreg Roach 4446ccdf4f0SGreg Roach while (!$stream->eof()) { 4456ccdf4f0SGreg Roach $file_data .= $stream->read(65536); 446a25f0a04SGreg Roach // There is no strrpos() function that searches for substrings :-( 447a25f0a04SGreg Roach for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) { 448a25f0a04SGreg Roach if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) { 449a25f0a04SGreg Roach // We’ve found the last record boundary in this chunk of data 450a25f0a04SGreg Roach break; 451a25f0a04SGreg Roach } 452a25f0a04SGreg Roach } 453a25f0a04SGreg Roach if ($pos) { 4541ad2dde6SGreg Roach DB::table('gedcom_chunk')->insert([ 4551ad2dde6SGreg Roach 'gedcom_id' => $this->id, 4561ad2dde6SGreg Roach 'chunk_data' => substr($file_data, 0, $pos), 457c1010edaSGreg Roach ]); 4581ad2dde6SGreg Roach 459a25f0a04SGreg Roach $file_data = substr($file_data, $pos); 460a25f0a04SGreg Roach } 461a25f0a04SGreg Roach } 4621ad2dde6SGreg Roach DB::table('gedcom_chunk')->insert([ 4631ad2dde6SGreg Roach 'gedcom_id' => $this->id, 4641ad2dde6SGreg Roach 'chunk_data' => $file_data, 465c1010edaSGreg Roach ]); 466a25f0a04SGreg Roach 4676ccdf4f0SGreg Roach $stream->close(); 4686ccdf4f0SGreg Roach } 4696ccdf4f0SGreg Roach 4706ccdf4f0SGreg Roach /** 4716ccdf4f0SGreg Roach * Create a new record from GEDCOM data. 4726ccdf4f0SGreg Roach * 4736ccdf4f0SGreg Roach * @param string $gedcom 4746ccdf4f0SGreg Roach * 4756ccdf4f0SGreg Roach * @return GedcomRecord|Individual|Family|Note|Source|Repository|Media 4766ccdf4f0SGreg Roach * @throws InvalidArgumentException 4776ccdf4f0SGreg Roach */ 4786ccdf4f0SGreg Roach public function createRecord(string $gedcom): GedcomRecord 4796ccdf4f0SGreg Roach { 4806ccdf4f0SGreg Roach if (!Str::startsWith($gedcom, '0 @@ ')) { 4816ccdf4f0SGreg Roach throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@'); 4826ccdf4f0SGreg Roach } 4836ccdf4f0SGreg Roach 4846ccdf4f0SGreg Roach $xref = $this->getNewXref(); 4856ccdf4f0SGreg Roach $gedcom = '0 @' . $xref . '@ ' . Str::after($gedcom, '0 @@ '); 4866ccdf4f0SGreg Roach 4876ccdf4f0SGreg Roach // Create a change record 4886ccdf4f0SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 4896ccdf4f0SGreg Roach 4906ccdf4f0SGreg Roach // Create a pending change 4916ccdf4f0SGreg Roach DB::table('change')->insert([ 4926ccdf4f0SGreg Roach 'gedcom_id' => $this->id, 4936ccdf4f0SGreg Roach 'xref' => $xref, 4946ccdf4f0SGreg Roach 'old_gedcom' => '', 4956ccdf4f0SGreg Roach 'new_gedcom' => $gedcom, 4966ccdf4f0SGreg Roach 'user_id' => Auth::id(), 4976ccdf4f0SGreg Roach ]); 4986ccdf4f0SGreg Roach 4996ccdf4f0SGreg Roach // Accept this pending change 5007c4add84SGreg Roach if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS)) { 50122e73debSGreg Roach $record = new GedcomRecord($xref, $gedcom, null, $this); 5026ccdf4f0SGreg Roach 50322e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 50422e73debSGreg Roach 50522e73debSGreg Roach return $record; 5066ccdf4f0SGreg Roach } 5076ccdf4f0SGreg Roach 5086ccdf4f0SGreg Roach return GedcomRecord::getInstance($xref, $this, $gedcom); 509a25f0a04SGreg Roach } 510304f20d5SGreg Roach 511304f20d5SGreg Roach /** 512b90d8accSGreg Roach * Generate a new XREF, unique across all family trees 513b90d8accSGreg Roach * 514b90d8accSGreg Roach * @return string 515b90d8accSGreg Roach */ 516771ae10aSGreg Roach public function getNewXref(): string 517c1010edaSGreg Roach { 518963fbaeeSGreg Roach // Lock the row, so that only one new XREF may be generated at a time. 519963fbaeeSGreg Roach DB::table('site_setting') 520963fbaeeSGreg Roach ->where('setting_name', '=', 'next_xref') 521963fbaeeSGreg Roach ->lockForUpdate() 522963fbaeeSGreg Roach ->get(); 523963fbaeeSGreg Roach 524a214e186SGreg Roach $prefix = 'X'; 525b90d8accSGreg Roach 526971d66c8SGreg Roach $increment = 1.0; 527b90d8accSGreg Roach do { 528963fbaeeSGreg Roach $num = (int) Site::getPreference('next_xref') + (int) $increment; 529971d66c8SGreg Roach 530971d66c8SGreg Roach // This exponential increment allows us to scan over large blocks of 531971d66c8SGreg Roach // existing data in a reasonable time. 532971d66c8SGreg Roach $increment *= 1.01; 533963fbaeeSGreg Roach 534963fbaeeSGreg Roach $xref = $prefix . $num; 535963fbaeeSGreg Roach 536963fbaeeSGreg Roach // Records may already exist with this sequence number. 537963fbaeeSGreg Roach $already_used = 538963fbaeeSGreg Roach DB::table('individuals')->where('i_id', '=', $xref)->exists() || 539963fbaeeSGreg Roach DB::table('families')->where('f_id', '=', $xref)->exists() || 540963fbaeeSGreg Roach DB::table('sources')->where('s_id', '=', $xref)->exists() || 541963fbaeeSGreg Roach DB::table('media')->where('m_id', '=', $xref)->exists() || 542963fbaeeSGreg Roach DB::table('other')->where('o_id', '=', $xref)->exists() || 543963fbaeeSGreg Roach DB::table('change')->where('xref', '=', $xref)->exists(); 544963fbaeeSGreg Roach } while ($already_used); 545963fbaeeSGreg Roach 546963fbaeeSGreg Roach Site::setPreference('next_xref', (string) $num); 547b90d8accSGreg Roach 548a214e186SGreg Roach return $xref; 549b90d8accSGreg Roach } 550b90d8accSGreg Roach 551b90d8accSGreg Roach /** 552afb591d7SGreg Roach * Create a new family from GEDCOM data. 553afb591d7SGreg Roach * 554afb591d7SGreg Roach * @param string $gedcom 555afb591d7SGreg Roach * 556afb591d7SGreg Roach * @return Family 557afb591d7SGreg Roach * @throws InvalidArgumentException 558afb591d7SGreg Roach */ 559afb591d7SGreg Roach public function createFamily(string $gedcom): GedcomRecord 560afb591d7SGreg Roach { 561bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ FAM')) { 562afb591d7SGreg Roach throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM'); 563afb591d7SGreg Roach } 564afb591d7SGreg Roach 565afb591d7SGreg Roach $xref = $this->getNewXref(); 566bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ FAM' . Str::after($gedcom, '0 @@ FAM'); 567afb591d7SGreg Roach 568afb591d7SGreg Roach // Create a change record 569e5a6b4d4SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 570afb591d7SGreg Roach 571afb591d7SGreg Roach // Create a pending change 572963fbaeeSGreg Roach DB::table('change')->insert([ 573963fbaeeSGreg Roach 'gedcom_id' => $this->id, 574963fbaeeSGreg Roach 'xref' => $xref, 575963fbaeeSGreg Roach 'old_gedcom' => '', 576963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 577963fbaeeSGreg Roach 'user_id' => Auth::id(), 578afb591d7SGreg Roach ]); 579304f20d5SGreg Roach 580304f20d5SGreg Roach // Accept this pending change 5817c4add84SGreg Roach if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') { 58222e73debSGreg Roach $record = new Family($xref, $gedcom, null, $this); 583afb591d7SGreg Roach 58422e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 58522e73debSGreg Roach 58622e73debSGreg Roach return $record; 587304f20d5SGreg Roach } 588afb591d7SGreg Roach 589afb591d7SGreg Roach return new Family($xref, '', $gedcom, $this); 590afb591d7SGreg Roach } 591afb591d7SGreg Roach 592afb591d7SGreg Roach /** 593afb591d7SGreg Roach * Create a new individual from GEDCOM data. 594afb591d7SGreg Roach * 595afb591d7SGreg Roach * @param string $gedcom 596afb591d7SGreg Roach * 597afb591d7SGreg Roach * @return Individual 598afb591d7SGreg Roach * @throws InvalidArgumentException 599afb591d7SGreg Roach */ 600afb591d7SGreg Roach public function createIndividual(string $gedcom): GedcomRecord 601afb591d7SGreg Roach { 602bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ INDI')) { 603afb591d7SGreg Roach throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI'); 604afb591d7SGreg Roach } 605afb591d7SGreg Roach 606afb591d7SGreg Roach $xref = $this->getNewXref(); 607bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ INDI' . Str::after($gedcom, '0 @@ INDI'); 608afb591d7SGreg Roach 609afb591d7SGreg Roach // Create a change record 610e5a6b4d4SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 611afb591d7SGreg Roach 612afb591d7SGreg Roach // Create a pending change 613963fbaeeSGreg Roach DB::table('change')->insert([ 614963fbaeeSGreg Roach 'gedcom_id' => $this->id, 615963fbaeeSGreg Roach 'xref' => $xref, 616963fbaeeSGreg Roach 'old_gedcom' => '', 617963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 618963fbaeeSGreg Roach 'user_id' => Auth::id(), 619afb591d7SGreg Roach ]); 620afb591d7SGreg Roach 621afb591d7SGreg Roach // Accept this pending change 6227c4add84SGreg Roach if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') { 62322e73debSGreg Roach $record = new Individual($xref, $gedcom, null, $this); 624afb591d7SGreg Roach 62522e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 62622e73debSGreg Roach 62722e73debSGreg Roach return $record; 628afb591d7SGreg Roach } 629afb591d7SGreg Roach 630afb591d7SGreg Roach return new Individual($xref, '', $gedcom, $this); 631304f20d5SGreg Roach } 6328586983fSGreg Roach 6338586983fSGreg Roach /** 63420b58d20SGreg Roach * Create a new media object from GEDCOM data. 63520b58d20SGreg Roach * 63620b58d20SGreg Roach * @param string $gedcom 63720b58d20SGreg Roach * 63820b58d20SGreg Roach * @return Media 63920b58d20SGreg Roach * @throws InvalidArgumentException 64020b58d20SGreg Roach */ 64120b58d20SGreg Roach public function createMediaObject(string $gedcom): Media 64220b58d20SGreg Roach { 643bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ OBJE')) { 64420b58d20SGreg Roach throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE'); 64520b58d20SGreg Roach } 64620b58d20SGreg Roach 64720b58d20SGreg Roach $xref = $this->getNewXref(); 648bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ OBJE' . Str::after($gedcom, '0 @@ OBJE'); 64920b58d20SGreg Roach 65020b58d20SGreg Roach // Create a change record 651e5a6b4d4SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 65220b58d20SGreg Roach 65320b58d20SGreg Roach // Create a pending change 654963fbaeeSGreg Roach DB::table('change')->insert([ 655963fbaeeSGreg Roach 'gedcom_id' => $this->id, 656963fbaeeSGreg Roach 'xref' => $xref, 657963fbaeeSGreg Roach 'old_gedcom' => '', 658963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 659963fbaeeSGreg Roach 'user_id' => Auth::id(), 66020b58d20SGreg Roach ]); 66120b58d20SGreg Roach 66220b58d20SGreg Roach // Accept this pending change 6637c4add84SGreg Roach if (Auth::user()->getPreference(User::PREF_AUTO_ACCEPT_EDITS) === '1') { 66422e73debSGreg Roach $record = new Media($xref, $gedcom, null, $this); 66520b58d20SGreg Roach 66622e73debSGreg Roach app(PendingChangesService::class)->acceptRecord($record); 66722e73debSGreg Roach 66822e73debSGreg Roach return $record; 66920b58d20SGreg Roach } 67020b58d20SGreg Roach 67120b58d20SGreg Roach return new Media($xref, '', $gedcom, $this); 67220b58d20SGreg Roach } 67320b58d20SGreg Roach 67420b58d20SGreg Roach /** 6758586983fSGreg Roach * What is the most significant individual in this tree. 6768586983fSGreg Roach * 677e5a6b4d4SGreg Roach * @param UserInterface $user 6783370567dSGreg Roach * @param string $xref 6798586983fSGreg Roach * 6808586983fSGreg Roach * @return Individual 6818586983fSGreg Roach */ 6823370567dSGreg Roach public function significantIndividual(UserInterface $user, $xref = ''): Individual 683c1010edaSGreg Roach { 6843370567dSGreg Roach if ($xref === '') { 6858f9b0fb2SGreg Roach $individual = null; 6863370567dSGreg Roach } else { 6873370567dSGreg Roach $individual = Individual::getInstance($xref, $this); 6883370567dSGreg Roach 6893370567dSGreg Roach if ($individual === null) { 6903370567dSGreg Roach $family = Family::getInstance($xref, $this); 6913370567dSGreg Roach 6923370567dSGreg Roach if ($family instanceof Family) { 6933370567dSGreg Roach $individual = $family->spouses()->first() ?? $family->children()->first(); 6943370567dSGreg Roach } 6953370567dSGreg Roach } 6963370567dSGreg Roach } 6978586983fSGreg Roach 698*6e91273cSGreg Roach if ($individual === null && $this->getUserPreference($user, User::PREF_TREE_DEFAULT_XREF) !== '') { 6997c4add84SGreg Roach $individual = Individual::getInstance($this->getUserPreference($user, User::PREF_TREE_DEFAULT_XREF), $this); 7008586983fSGreg Roach } 7018f9b0fb2SGreg Roach 7027c4add84SGreg Roach if ($individual === null && $this->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF) !== '') { 7037c4add84SGreg Roach $individual = Individual::getInstance($this->getUserPreference($user, User::PREF_TREE_ACCOUNT_XREF), $this); 7048586983fSGreg Roach } 7058f9b0fb2SGreg Roach 706bec87e94SGreg Roach if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') { 7078586983fSGreg Roach $individual = Individual::getInstance($this->getPreference('PEDIGREE_ROOT_ID'), $this); 7088586983fSGreg Roach } 7098f9b0fb2SGreg Roach if ($individual === null) { 7108f9b0fb2SGreg Roach $xref = (string) DB::table('individuals') 7118f9b0fb2SGreg Roach ->where('i_file', '=', $this->id()) 7128f9b0fb2SGreg Roach ->min('i_id'); 713769d7d6eSGreg Roach 714769d7d6eSGreg Roach $individual = Individual::getInstance($xref, $this); 7155fe1add5SGreg Roach } 7168f9b0fb2SGreg Roach if ($individual === null) { 7175fe1add5SGreg Roach // always return a record 7185fe1add5SGreg Roach $individual = new Individual('I', '0 @I@ INDI', null, $this); 7195fe1add5SGreg Roach } 7205fe1add5SGreg Roach 7215fe1add5SGreg Roach return $individual; 7225fe1add5SGreg Roach } 7231df7ae39SGreg Roach 72485a166d8SGreg Roach /** 72585a166d8SGreg Roach * Where do we store our media files. 72685a166d8SGreg Roach * 727a04bb9a2SGreg Roach * @param FilesystemInterface $data_filesystem 728a04bb9a2SGreg Roach * 72985a166d8SGreg Roach * @return FilesystemInterface 73085a166d8SGreg Roach */ 731a04bb9a2SGreg Roach public function mediaFilesystem(FilesystemInterface $data_filesystem): FilesystemInterface 7321df7ae39SGreg Roach { 733456d0d35SGreg Roach $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/'); 734a04bb9a2SGreg Roach $adapter = new ChrootAdapter($data_filesystem, $media_dir); 735456d0d35SGreg Roach 736456d0d35SGreg Roach return new Filesystem($adapter); 7371df7ae39SGreg Roach } 738a25f0a04SGreg Roach} 739