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; 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 38a25f0a04SGreg Roach/** 3976692c8bSGreg Roach * Provide an interface to the wt_gedcom table. 40a25f0a04SGreg Roach */ 41c1010edaSGreg Roachclass Tree 42c1010edaSGreg Roach{ 43061b43d7SGreg Roach private const RESN_PRIVACY = [ 44061b43d7SGreg Roach 'none' => Auth::PRIV_PRIVATE, 45061b43d7SGreg Roach 'privacy' => Auth::PRIV_USER, 46061b43d7SGreg Roach 'confidential' => Auth::PRIV_NONE, 47061b43d7SGreg Roach 'hidden' => Auth::PRIV_HIDE, 48061b43d7SGreg Roach ]; 493df1e584SGreg Roach 506ccdf4f0SGreg Roach /** @var int The tree's ID number */ 516ccdf4f0SGreg Roach private $id; 523df1e584SGreg Roach 536ccdf4f0SGreg Roach /** @var string The tree's name */ 546ccdf4f0SGreg Roach private $name; 553df1e584SGreg Roach 566ccdf4f0SGreg Roach /** @var string The tree's title */ 576ccdf4f0SGreg Roach private $title; 583df1e584SGreg Roach 596ccdf4f0SGreg Roach /** @var int[] Default access rules for facts in this tree */ 606ccdf4f0SGreg Roach private $fact_privacy; 613df1e584SGreg Roach 626ccdf4f0SGreg Roach /** @var int[] Default access rules for individuals in this tree */ 636ccdf4f0SGreg Roach private $individual_privacy; 643df1e584SGreg Roach 656ccdf4f0SGreg Roach /** @var integer[][] Default access rules for individual facts in this tree */ 666ccdf4f0SGreg Roach private $individual_fact_privacy; 673df1e584SGreg Roach 686ccdf4f0SGreg Roach /** @var string[] Cached copy of the wt_gedcom_setting table. */ 696ccdf4f0SGreg Roach private $preferences = []; 703df1e584SGreg Roach 716ccdf4f0SGreg Roach /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */ 726ccdf4f0SGreg Roach private $user_preferences = []; 73061b43d7SGreg Roach 74a25f0a04SGreg Roach /** 753df1e584SGreg Roach * Create a tree object. 76a25f0a04SGreg Roach * 7772cf66d4SGreg Roach * @param int $id 78aa6f03bbSGreg Roach * @param string $name 79cc13d6d8SGreg Roach * @param string $title 80a25f0a04SGreg Roach */ 815afbc57aSGreg Roach public function __construct(int $id, string $name, string $title) 82c1010edaSGreg Roach { 8372cf66d4SGreg Roach $this->id = $id; 84aa6f03bbSGreg Roach $this->name = $name; 85cc13d6d8SGreg Roach $this->title = $title; 8613abd6f3SGreg Roach $this->fact_privacy = []; 8713abd6f3SGreg Roach $this->individual_privacy = []; 8813abd6f3SGreg Roach $this->individual_fact_privacy = []; 89518bbdc1SGreg Roach 90518bbdc1SGreg Roach // Load the privacy settings for this tree 91061b43d7SGreg Roach $rows = DB::table('default_resn') 92061b43d7SGreg Roach ->where('gedcom_id', '=', $this->id) 93061b43d7SGreg Roach ->get(); 94518bbdc1SGreg Roach 95518bbdc1SGreg Roach foreach ($rows as $row) { 96061b43d7SGreg Roach // Convert GEDCOM privacy restriction to a webtrees access level. 97061b43d7SGreg Roach $row->resn = self::RESN_PRIVACY[$row->resn]; 98061b43d7SGreg Roach 99518bbdc1SGreg Roach if ($row->xref !== null) { 100518bbdc1SGreg Roach if ($row->tag_type !== null) { 101*b262b3d3SGreg Roach $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn; 102518bbdc1SGreg Roach } else { 103*b262b3d3SGreg Roach $this->individual_privacy[$row->xref] = $row->resn; 104518bbdc1SGreg Roach } 105518bbdc1SGreg Roach } else { 106*b262b3d3SGreg Roach $this->fact_privacy[$row->tag_type] = $row->resn; 107518bbdc1SGreg Roach } 108518bbdc1SGreg Roach } 109a25f0a04SGreg Roach } 110a25f0a04SGreg Roach 111a25f0a04SGreg Roach /** 1125afbc57aSGreg Roach * A closure which will create a record from a database row. 1135afbc57aSGreg Roach * 1145afbc57aSGreg Roach * @return Closure 1155afbc57aSGreg Roach */ 1165afbc57aSGreg Roach public static function rowMapper(): Closure 1175afbc57aSGreg Roach { 1185afbc57aSGreg Roach return static function (stdClass $row): Tree { 1195afbc57aSGreg Roach return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title); 1205afbc57aSGreg Roach }; 1215afbc57aSGreg Roach } 1225afbc57aSGreg Roach 1235afbc57aSGreg Roach /** 1246ccdf4f0SGreg Roach * Find the tree with a specific ID. 125a25f0a04SGreg Roach * 1266ccdf4f0SGreg Roach * @param int $tree_id 1276ccdf4f0SGreg Roach * 1286ccdf4f0SGreg Roach * @return Tree 129a25f0a04SGreg Roach */ 1306ccdf4f0SGreg Roach public static function findById(int $tree_id): Tree 131c1010edaSGreg Roach { 1326ccdf4f0SGreg Roach return self::getAll()[$tree_id]; 133a25f0a04SGreg Roach } 134a25f0a04SGreg Roach 135a25f0a04SGreg Roach /** 1366ccdf4f0SGreg Roach * Fetch all the trees that we have permission to access. 137a25f0a04SGreg Roach * 1386ccdf4f0SGreg Roach * @return Tree[] 1395afbc57aSGreg Roach * @deprecated 140a25f0a04SGreg Roach */ 1416ccdf4f0SGreg Roach public static function getAll(): array 142c1010edaSGreg Roach { 1433df1e584SGreg Roach return (new TreeService())->all()->all(); 144a25f0a04SGreg Roach } 145a25f0a04SGreg Roach 146a25f0a04SGreg Roach /** 1478b67c11aSGreg Roach * All the trees that we have permission to access. 148a25f0a04SGreg Roach * 14954c7f8dfSGreg Roach * @return Collection 1505afbc57aSGreg Roach * @deprecated 151a25f0a04SGreg Roach */ 1528b67c11aSGreg Roach public static function all(): Collection 153c1010edaSGreg Roach { 1545afbc57aSGreg Roach return (new TreeService())->all(); 155a25f0a04SGreg Roach } 1568b67c11aSGreg Roach 1578b67c11aSGreg Roach /** 158a25f0a04SGreg Roach * Create arguments to select_edit_control() 159a25f0a04SGreg Roach * Note - these will be escaped later 160a25f0a04SGreg Roach * 161a25f0a04SGreg Roach * @return string[] 162a25f0a04SGreg Roach */ 163771ae10aSGreg Roach public static function getIdList(): array 164c1010edaSGreg Roach { 16513abd6f3SGreg Roach $list = []; 166a25f0a04SGreg Roach foreach (self::getAll() as $tree) { 16772cf66d4SGreg Roach $list[$tree->id] = $tree->title; 168a25f0a04SGreg Roach } 169a25f0a04SGreg Roach 170a25f0a04SGreg Roach return $list; 171a25f0a04SGreg Roach } 172a25f0a04SGreg Roach 173a25f0a04SGreg Roach /** 174a25f0a04SGreg Roach * Create arguments to select_edit_control() 175a25f0a04SGreg Roach * Note - these will be escaped later 176a25f0a04SGreg Roach * 177a25f0a04SGreg Roach * @return string[] 178a25f0a04SGreg Roach */ 179771ae10aSGreg Roach public static function getNameList(): array 180c1010edaSGreg Roach { 18113abd6f3SGreg Roach $list = []; 182a25f0a04SGreg Roach foreach (self::getAll() as $tree) { 183a25f0a04SGreg Roach $list[$tree->name] = $tree->title; 184a25f0a04SGreg Roach } 185a25f0a04SGreg Roach 186a25f0a04SGreg Roach return $list; 187a25f0a04SGreg Roach } 188a25f0a04SGreg Roach 189a25f0a04SGreg Roach /** 190a25f0a04SGreg Roach * Create a new tree 191a25f0a04SGreg Roach * 192a25f0a04SGreg Roach * @param string $tree_name 193a25f0a04SGreg Roach * @param string $tree_title 194a25f0a04SGreg Roach * 195a25f0a04SGreg Roach * @return Tree 1965afbc57aSGreg Roach * @deprecated 197a25f0a04SGreg Roach */ 198771ae10aSGreg Roach public static function create(string $tree_name, string $tree_title): Tree 199c1010edaSGreg Roach { 2005afbc57aSGreg Roach return (new TreeService())->create($tree_name, $tree_title); 201a25f0a04SGreg Roach } 202a25f0a04SGreg Roach 203a25f0a04SGreg Roach /** 2046ccdf4f0SGreg Roach * Find the tree with a specific name. 2056ccdf4f0SGreg Roach * 2065afbc57aSGreg Roach * @param string $name 2076ccdf4f0SGreg Roach * 2086ccdf4f0SGreg Roach * @return Tree|null 2095afbc57aSGreg Roach * @deprecated 2106ccdf4f0SGreg Roach */ 2115afbc57aSGreg Roach public static function findByName($name): ?Tree 2126ccdf4f0SGreg Roach { 2136ccdf4f0SGreg Roach foreach (self::getAll() as $tree) { 2145afbc57aSGreg Roach if ($tree->name === $name) { 2156ccdf4f0SGreg Roach return $tree; 2166ccdf4f0SGreg Roach } 2176ccdf4f0SGreg Roach } 2186ccdf4f0SGreg Roach 2196ccdf4f0SGreg Roach return null; 2206ccdf4f0SGreg Roach } 2216ccdf4f0SGreg Roach 2226ccdf4f0SGreg Roach /** 2236ccdf4f0SGreg Roach * Set the tree’s configuration settings. 2246ccdf4f0SGreg Roach * 2256ccdf4f0SGreg Roach * @param string $setting_name 2266ccdf4f0SGreg Roach * @param string $setting_value 2276ccdf4f0SGreg Roach * 2286ccdf4f0SGreg Roach * @return $this 2296ccdf4f0SGreg Roach */ 2306ccdf4f0SGreg Roach public function setPreference(string $setting_name, string $setting_value): Tree 2316ccdf4f0SGreg Roach { 2326ccdf4f0SGreg Roach if ($setting_value !== $this->getPreference($setting_name)) { 2336ccdf4f0SGreg Roach DB::table('gedcom_setting')->updateOrInsert([ 2346ccdf4f0SGreg Roach 'gedcom_id' => $this->id, 2356ccdf4f0SGreg Roach 'setting_name' => $setting_name, 2366ccdf4f0SGreg Roach ], [ 2376ccdf4f0SGreg Roach 'setting_value' => $setting_value, 2386ccdf4f0SGreg Roach ]); 2396ccdf4f0SGreg Roach 2406ccdf4f0SGreg Roach $this->preferences[$setting_name] = $setting_value; 2416ccdf4f0SGreg Roach 2426ccdf4f0SGreg Roach Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this); 2436ccdf4f0SGreg Roach } 2446ccdf4f0SGreg Roach 2456ccdf4f0SGreg Roach return $this; 2466ccdf4f0SGreg Roach } 2476ccdf4f0SGreg Roach 2486ccdf4f0SGreg Roach /** 2496ccdf4f0SGreg Roach * Get the tree’s configuration settings. 2506ccdf4f0SGreg Roach * 2516ccdf4f0SGreg Roach * @param string $setting_name 2526ccdf4f0SGreg Roach * @param string $default 2536ccdf4f0SGreg Roach * 2546ccdf4f0SGreg Roach * @return string 2556ccdf4f0SGreg Roach */ 2566ccdf4f0SGreg Roach public function getPreference(string $setting_name, string $default = ''): string 2576ccdf4f0SGreg Roach { 25854c1ab5eSGreg Roach if ($this->preferences === []) { 2596ccdf4f0SGreg Roach $this->preferences = DB::table('gedcom_setting') 2606ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 2616ccdf4f0SGreg Roach ->pluck('setting_value', 'setting_name') 2626ccdf4f0SGreg Roach ->all(); 2636ccdf4f0SGreg Roach } 2646ccdf4f0SGreg Roach 2656ccdf4f0SGreg Roach return $this->preferences[$setting_name] ?? $default; 2666ccdf4f0SGreg Roach } 2676ccdf4f0SGreg Roach 2686ccdf4f0SGreg Roach /** 2696ccdf4f0SGreg Roach * The name of this tree 2706ccdf4f0SGreg Roach * 2716ccdf4f0SGreg Roach * @return string 2726ccdf4f0SGreg Roach */ 2736ccdf4f0SGreg Roach public function name(): string 2746ccdf4f0SGreg Roach { 2756ccdf4f0SGreg Roach return $this->name; 2766ccdf4f0SGreg Roach } 2776ccdf4f0SGreg Roach 2786ccdf4f0SGreg Roach /** 2796ccdf4f0SGreg Roach * The title of this tree 2806ccdf4f0SGreg Roach * 2816ccdf4f0SGreg Roach * @return string 2826ccdf4f0SGreg Roach */ 2836ccdf4f0SGreg Roach public function title(): string 2846ccdf4f0SGreg Roach { 2856ccdf4f0SGreg Roach return $this->title; 2866ccdf4f0SGreg Roach } 2876ccdf4f0SGreg Roach 2886ccdf4f0SGreg Roach /** 2896ccdf4f0SGreg Roach * The fact-level privacy for this tree. 2906ccdf4f0SGreg Roach * 2916ccdf4f0SGreg Roach * @return int[] 2926ccdf4f0SGreg Roach */ 2936ccdf4f0SGreg Roach public function getFactPrivacy(): array 2946ccdf4f0SGreg Roach { 2956ccdf4f0SGreg Roach return $this->fact_privacy; 2966ccdf4f0SGreg Roach } 2976ccdf4f0SGreg Roach 2986ccdf4f0SGreg Roach /** 2996ccdf4f0SGreg Roach * The individual-level privacy for this tree. 3006ccdf4f0SGreg Roach * 3016ccdf4f0SGreg Roach * @return int[] 3026ccdf4f0SGreg Roach */ 3036ccdf4f0SGreg Roach public function getIndividualPrivacy(): array 3046ccdf4f0SGreg Roach { 3056ccdf4f0SGreg Roach return $this->individual_privacy; 3066ccdf4f0SGreg Roach } 3076ccdf4f0SGreg Roach 3086ccdf4f0SGreg Roach /** 3096ccdf4f0SGreg Roach * The individual-fact-level privacy for this tree. 3106ccdf4f0SGreg Roach * 3116ccdf4f0SGreg Roach * @return int[][] 3126ccdf4f0SGreg Roach */ 3136ccdf4f0SGreg Roach public function getIndividualFactPrivacy(): array 3146ccdf4f0SGreg Roach { 3156ccdf4f0SGreg Roach return $this->individual_fact_privacy; 3166ccdf4f0SGreg Roach } 3176ccdf4f0SGreg Roach 3186ccdf4f0SGreg Roach /** 3196ccdf4f0SGreg Roach * Set the tree’s user-configuration settings. 3206ccdf4f0SGreg Roach * 3216ccdf4f0SGreg Roach * @param UserInterface $user 3226ccdf4f0SGreg Roach * @param string $setting_name 3236ccdf4f0SGreg Roach * @param string $setting_value 3246ccdf4f0SGreg Roach * 3256ccdf4f0SGreg Roach * @return $this 3266ccdf4f0SGreg Roach */ 3276ccdf4f0SGreg Roach public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree 3286ccdf4f0SGreg Roach { 3296ccdf4f0SGreg Roach if ($this->getUserPreference($user, $setting_name) !== $setting_value) { 3306ccdf4f0SGreg Roach // Update the database 3316ccdf4f0SGreg Roach DB::table('user_gedcom_setting')->updateOrInsert([ 3326ccdf4f0SGreg Roach 'gedcom_id' => $this->id(), 3336ccdf4f0SGreg Roach 'user_id' => $user->id(), 3346ccdf4f0SGreg Roach 'setting_name' => $setting_name, 3356ccdf4f0SGreg Roach ], [ 3366ccdf4f0SGreg Roach 'setting_value' => $setting_value, 3376ccdf4f0SGreg Roach ]); 3386ccdf4f0SGreg Roach 3396ccdf4f0SGreg Roach // Update the cache 3406ccdf4f0SGreg Roach $this->user_preferences[$user->id()][$setting_name] = $setting_value; 3416ccdf4f0SGreg Roach // Audit log of changes 3426ccdf4f0SGreg Roach Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this); 3436ccdf4f0SGreg Roach } 3446ccdf4f0SGreg Roach 3456ccdf4f0SGreg Roach return $this; 3466ccdf4f0SGreg Roach } 3476ccdf4f0SGreg Roach 3486ccdf4f0SGreg Roach /** 3496ccdf4f0SGreg Roach * Get the tree’s user-configuration settings. 3506ccdf4f0SGreg Roach * 3516ccdf4f0SGreg Roach * @param UserInterface $user 3526ccdf4f0SGreg Roach * @param string $setting_name 3536ccdf4f0SGreg Roach * @param string $default 3546ccdf4f0SGreg Roach * 3556ccdf4f0SGreg Roach * @return string 3566ccdf4f0SGreg Roach */ 3576ccdf4f0SGreg Roach public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string 3586ccdf4f0SGreg Roach { 3596ccdf4f0SGreg Roach // There are lots of settings, and we need to fetch lots of them on every page 3606ccdf4f0SGreg Roach // so it is quicker to fetch them all in one go. 3616ccdf4f0SGreg Roach if (!array_key_exists($user->id(), $this->user_preferences)) { 3626ccdf4f0SGreg Roach $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting') 3636ccdf4f0SGreg Roach ->where('user_id', '=', $user->id()) 3646ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 3656ccdf4f0SGreg Roach ->pluck('setting_value', 'setting_name') 3666ccdf4f0SGreg Roach ->all(); 3676ccdf4f0SGreg Roach } 3686ccdf4f0SGreg Roach 3696ccdf4f0SGreg Roach return $this->user_preferences[$user->id()][$setting_name] ?? $default; 3706ccdf4f0SGreg Roach } 3716ccdf4f0SGreg Roach 3726ccdf4f0SGreg Roach /** 3736ccdf4f0SGreg Roach * The ID of this tree 3746ccdf4f0SGreg Roach * 3756ccdf4f0SGreg Roach * @return int 3766ccdf4f0SGreg Roach */ 3776ccdf4f0SGreg Roach public function id(): int 3786ccdf4f0SGreg Roach { 3796ccdf4f0SGreg Roach return $this->id; 3806ccdf4f0SGreg Roach } 3816ccdf4f0SGreg Roach 3826ccdf4f0SGreg Roach /** 3836ccdf4f0SGreg Roach * Can a user accept changes for this tree? 3846ccdf4f0SGreg Roach * 3856ccdf4f0SGreg Roach * @param UserInterface $user 3866ccdf4f0SGreg Roach * 3876ccdf4f0SGreg Roach * @return bool 3886ccdf4f0SGreg Roach */ 3896ccdf4f0SGreg Roach public function canAcceptChanges(UserInterface $user): bool 3906ccdf4f0SGreg Roach { 3916ccdf4f0SGreg Roach return Auth::isModerator($this, $user); 3926ccdf4f0SGreg Roach } 3936ccdf4f0SGreg Roach 3946ccdf4f0SGreg Roach /** 395b78374c5SGreg Roach * Are there any pending edits for this tree, than need reviewing by a moderator. 396b78374c5SGreg Roach * 397b78374c5SGreg Roach * @return bool 398b78374c5SGreg Roach */ 399771ae10aSGreg Roach public function hasPendingEdit(): bool 400c1010edaSGreg Roach { 40115a3f100SGreg Roach return DB::table('change') 40215a3f100SGreg Roach ->where('gedcom_id', '=', $this->id) 40315a3f100SGreg Roach ->where('status', '=', 'pending') 40415a3f100SGreg Roach ->exists(); 405b78374c5SGreg Roach } 406b78374c5SGreg Roach 407b78374c5SGreg Roach /** 4086ccdf4f0SGreg Roach * Delete everything relating to a tree 4096ccdf4f0SGreg Roach * 4106ccdf4f0SGreg Roach * @return void 4116ccdf4f0SGreg Roach */ 4126ccdf4f0SGreg Roach public function delete(): void 4136ccdf4f0SGreg Roach { 4146ccdf4f0SGreg Roach // If this is the default tree, then unset it 4156ccdf4f0SGreg Roach if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) { 4166ccdf4f0SGreg Roach Site::setPreference('DEFAULT_GEDCOM', ''); 4176ccdf4f0SGreg Roach } 4186ccdf4f0SGreg Roach 4196ccdf4f0SGreg Roach $this->deleteGenealogyData(false); 4206ccdf4f0SGreg Roach 4216ccdf4f0SGreg Roach DB::table('block_setting') 4226ccdf4f0SGreg Roach ->join('block', 'block.block_id', '=', 'block_setting.block_id') 4236ccdf4f0SGreg Roach ->where('gedcom_id', '=', $this->id) 4246ccdf4f0SGreg Roach ->delete(); 4256ccdf4f0SGreg Roach DB::table('block')->where('gedcom_id', '=', $this->id)->delete(); 4266ccdf4f0SGreg Roach DB::table('user_gedcom_setting')->where('gedcom_id', '=', $this->id)->delete(); 4276ccdf4f0SGreg Roach DB::table('gedcom_setting')->where('gedcom_id', '=', $this->id)->delete(); 4286ccdf4f0SGreg Roach DB::table('module_privacy')->where('gedcom_id', '=', $this->id)->delete(); 4296ccdf4f0SGreg Roach DB::table('hit_counter')->where('gedcom_id', '=', $this->id)->delete(); 4306ccdf4f0SGreg Roach DB::table('default_resn')->where('gedcom_id', '=', $this->id)->delete(); 4316ccdf4f0SGreg Roach DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete(); 4326ccdf4f0SGreg Roach DB::table('log')->where('gedcom_id', '=', $this->id)->delete(); 4336ccdf4f0SGreg Roach DB::table('gedcom')->where('gedcom_id', '=', $this->id)->delete(); 4346ccdf4f0SGreg Roach } 4356ccdf4f0SGreg Roach 4366ccdf4f0SGreg Roach /** 437a25f0a04SGreg Roach * Delete all the genealogy data from a tree - in preparation for importing 438a25f0a04SGreg Roach * new data. Optionally retain the media data, for when the user has been 439a25f0a04SGreg Roach * editing their data offline using an application which deletes (or does not 440a25f0a04SGreg Roach * support) media data. 441a25f0a04SGreg Roach * 442a25f0a04SGreg Roach * @param bool $keep_media 443b7e60af1SGreg Roach * 444b7e60af1SGreg Roach * @return void 445a25f0a04SGreg Roach */ 446e364afe4SGreg Roach public function deleteGenealogyData(bool $keep_media): void 447c1010edaSGreg Roach { 4481ad2dde6SGreg Roach DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete(); 4491ad2dde6SGreg Roach DB::table('individuals')->where('i_file', '=', $this->id)->delete(); 4501ad2dde6SGreg Roach DB::table('families')->where('f_file', '=', $this->id)->delete(); 4511ad2dde6SGreg Roach DB::table('sources')->where('s_file', '=', $this->id)->delete(); 4521ad2dde6SGreg Roach DB::table('other')->where('o_file', '=', $this->id)->delete(); 4531ad2dde6SGreg Roach DB::table('places')->where('p_file', '=', $this->id)->delete(); 4541ad2dde6SGreg Roach DB::table('placelinks')->where('pl_file', '=', $this->id)->delete(); 4551ad2dde6SGreg Roach DB::table('name')->where('n_file', '=', $this->id)->delete(); 4561ad2dde6SGreg Roach DB::table('dates')->where('d_file', '=', $this->id)->delete(); 4571ad2dde6SGreg Roach DB::table('change')->where('gedcom_id', '=', $this->id)->delete(); 458a25f0a04SGreg Roach 459a25f0a04SGreg Roach if ($keep_media) { 4601ad2dde6SGreg Roach DB::table('link')->where('l_file', '=', $this->id) 4611ad2dde6SGreg Roach ->where('l_type', '<>', 'OBJE') 4621ad2dde6SGreg Roach ->delete(); 463a25f0a04SGreg Roach } else { 4641ad2dde6SGreg Roach DB::table('link')->where('l_file', '=', $this->id)->delete(); 4651ad2dde6SGreg Roach DB::table('media_file')->where('m_file', '=', $this->id)->delete(); 4661ad2dde6SGreg Roach DB::table('media')->where('m_file', '=', $this->id)->delete(); 467a25f0a04SGreg Roach } 468a25f0a04SGreg Roach } 469a25f0a04SGreg Roach 470a25f0a04SGreg Roach /** 471a25f0a04SGreg Roach * Export the tree to a GEDCOM file 472a25f0a04SGreg Roach * 4735792757eSGreg Roach * @param resource $stream 474b7e60af1SGreg Roach * 475b7e60af1SGreg Roach * @return void 476a25f0a04SGreg Roach */ 477425af8b9SGreg Roach public function exportGedcom($stream): void 478c1010edaSGreg Roach { 479a3d8780cSGreg Roach $buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this, 'UTF-8')); 48094026f20SGreg Roach 48194026f20SGreg Roach $union_families = DB::table('families') 48294026f20SGreg Roach ->where('f_file', '=', $this->id) 483a69f5655SGreg Roach ->select(['f_gedcom AS gedcom', 'f_id AS xref', new Expression('LENGTH(f_id) AS len'), new Expression('2 AS n')]); 48494026f20SGreg Roach 48594026f20SGreg Roach $union_sources = DB::table('sources') 48694026f20SGreg Roach ->where('s_file', '=', $this->id) 487a69f5655SGreg Roach ->select(['s_gedcom AS gedcom', 's_id AS xref', new Expression('LENGTH(s_id) AS len'), new Expression('3 AS n')]); 48894026f20SGreg Roach 48994026f20SGreg Roach $union_other = DB::table('other') 49094026f20SGreg Roach ->where('o_file', '=', $this->id) 49194026f20SGreg Roach ->whereNotIn('o_type', ['HEAD', 'TRLR']) 492a69f5655SGreg Roach ->select(['o_gedcom AS gedcom', 'o_id AS xref', new Expression('LENGTH(o_id) AS len'), new Expression('4 AS n')]); 49394026f20SGreg Roach 49494026f20SGreg Roach $union_media = DB::table('media') 49594026f20SGreg Roach ->where('m_file', '=', $this->id) 496a69f5655SGreg Roach ->select(['m_gedcom AS gedcom', 'm_id AS xref', new Expression('LENGTH(m_id) AS len'), new Expression('5 AS n')]); 49794026f20SGreg Roach 498e5a6b4d4SGreg Roach DB::table('individuals') 49994026f20SGreg Roach ->where('i_file', '=', $this->id) 500a69f5655SGreg Roach ->select(['i_gedcom AS gedcom', 'i_id AS xref', new Expression('LENGTH(i_id) AS len'), new Expression('1 AS n')]) 50194026f20SGreg Roach ->union($union_families) 50294026f20SGreg Roach ->union($union_sources) 50394026f20SGreg Roach ->union($union_other) 50494026f20SGreg Roach ->union($union_media) 50594026f20SGreg Roach ->orderBy('n') 50694026f20SGreg Roach ->orderBy('len') 50794026f20SGreg Roach ->orderBy('xref') 50827825e0aSGreg Roach ->chunk(1000, static function (Collection $rows) use ($stream, &$buffer): void { 50994026f20SGreg Roach foreach ($rows as $row) { 5103d7a8a4cSGreg Roach $buffer .= FunctionsExport::reformatRecord($row->gedcom); 511a25f0a04SGreg Roach if (strlen($buffer) > 65535) { 5125792757eSGreg Roach fwrite($stream, $buffer); 513a25f0a04SGreg Roach $buffer = ''; 514a25f0a04SGreg Roach } 515a25f0a04SGreg Roach } 51694026f20SGreg Roach }); 51794026f20SGreg Roach 5180f471f91SGreg Roach fwrite($stream, $buffer . '0 TRLR' . Gedcom::EOL); 519a25f0a04SGreg Roach } 520a25f0a04SGreg Roach 521a25f0a04SGreg Roach /** 522a25f0a04SGreg Roach * Import data from a gedcom file into this tree. 523a25f0a04SGreg Roach * 5246ccdf4f0SGreg Roach * @param StreamInterface $stream The GEDCOM file. 525a25f0a04SGreg Roach * @param string $filename The preferred filename, for export/download. 526a25f0a04SGreg Roach * 527b7e60af1SGreg Roach * @return void 528a25f0a04SGreg Roach */ 5296ccdf4f0SGreg Roach public function importGedcomFile(StreamInterface $stream, string $filename): void 530c1010edaSGreg Roach { 531a25f0a04SGreg Roach // Read the file in blocks of roughly 64K. Ensure that each block 532a25f0a04SGreg Roach // contains complete gedcom records. This will ensure we don’t split 533a25f0a04SGreg Roach // multi-byte characters, as well as simplifying the code to import 534a25f0a04SGreg Roach // each block. 535a25f0a04SGreg Roach 536a25f0a04SGreg Roach $file_data = ''; 537a25f0a04SGreg Roach 538b7e60af1SGreg Roach $this->deleteGenealogyData((bool) $this->getPreference('keep_media')); 539a25f0a04SGreg Roach $this->setPreference('gedcom_filename', $filename); 540a25f0a04SGreg Roach $this->setPreference('imported', '0'); 541a25f0a04SGreg Roach 5426ccdf4f0SGreg Roach while (!$stream->eof()) { 5436ccdf4f0SGreg Roach $file_data .= $stream->read(65536); 544a25f0a04SGreg Roach // There is no strrpos() function that searches for substrings :-( 545a25f0a04SGreg Roach for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) { 546a25f0a04SGreg Roach if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) { 547a25f0a04SGreg Roach // We’ve found the last record boundary in this chunk of data 548a25f0a04SGreg Roach break; 549a25f0a04SGreg Roach } 550a25f0a04SGreg Roach } 551a25f0a04SGreg Roach if ($pos) { 5521ad2dde6SGreg Roach DB::table('gedcom_chunk')->insert([ 5531ad2dde6SGreg Roach 'gedcom_id' => $this->id, 5541ad2dde6SGreg Roach 'chunk_data' => substr($file_data, 0, $pos), 555c1010edaSGreg Roach ]); 5561ad2dde6SGreg Roach 557a25f0a04SGreg Roach $file_data = substr($file_data, $pos); 558a25f0a04SGreg Roach } 559a25f0a04SGreg Roach } 5601ad2dde6SGreg Roach DB::table('gedcom_chunk')->insert([ 5611ad2dde6SGreg Roach 'gedcom_id' => $this->id, 5621ad2dde6SGreg Roach 'chunk_data' => $file_data, 563c1010edaSGreg Roach ]); 564a25f0a04SGreg Roach 5656ccdf4f0SGreg Roach $stream->close(); 5666ccdf4f0SGreg Roach } 5676ccdf4f0SGreg Roach 5686ccdf4f0SGreg Roach /** 5696ccdf4f0SGreg Roach * Create a new record from GEDCOM data. 5706ccdf4f0SGreg Roach * 5716ccdf4f0SGreg Roach * @param string $gedcom 5726ccdf4f0SGreg Roach * 5736ccdf4f0SGreg Roach * @return GedcomRecord|Individual|Family|Note|Source|Repository|Media 5746ccdf4f0SGreg Roach * @throws InvalidArgumentException 5756ccdf4f0SGreg Roach */ 5766ccdf4f0SGreg Roach public function createRecord(string $gedcom): GedcomRecord 5776ccdf4f0SGreg Roach { 5786ccdf4f0SGreg Roach if (!Str::startsWith($gedcom, '0 @@ ')) { 5796ccdf4f0SGreg Roach throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@'); 5806ccdf4f0SGreg Roach } 5816ccdf4f0SGreg Roach 5826ccdf4f0SGreg Roach $xref = $this->getNewXref(); 5836ccdf4f0SGreg Roach $gedcom = '0 @' . $xref . '@ ' . Str::after($gedcom, '0 @@ '); 5846ccdf4f0SGreg Roach 5856ccdf4f0SGreg Roach // Create a change record 5866ccdf4f0SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 5876ccdf4f0SGreg Roach 5886ccdf4f0SGreg Roach // Create a pending change 5896ccdf4f0SGreg Roach DB::table('change')->insert([ 5906ccdf4f0SGreg Roach 'gedcom_id' => $this->id, 5916ccdf4f0SGreg Roach 'xref' => $xref, 5926ccdf4f0SGreg Roach 'old_gedcom' => '', 5936ccdf4f0SGreg Roach 'new_gedcom' => $gedcom, 5946ccdf4f0SGreg Roach 'user_id' => Auth::id(), 5956ccdf4f0SGreg Roach ]); 5966ccdf4f0SGreg Roach 5976ccdf4f0SGreg Roach // Accept this pending change 5986ccdf4f0SGreg Roach if (Auth::user()->getPreference('auto_accept')) { 5996ccdf4f0SGreg Roach FunctionsImport::acceptAllChanges($xref, $this); 6006ccdf4f0SGreg Roach 6016ccdf4f0SGreg Roach return new GedcomRecord($xref, $gedcom, null, $this); 6026ccdf4f0SGreg Roach } 6036ccdf4f0SGreg Roach 6046ccdf4f0SGreg Roach return GedcomRecord::getInstance($xref, $this, $gedcom); 605a25f0a04SGreg Roach } 606304f20d5SGreg Roach 607304f20d5SGreg Roach /** 608b90d8accSGreg Roach * Generate a new XREF, unique across all family trees 609b90d8accSGreg Roach * 610b90d8accSGreg Roach * @return string 611b90d8accSGreg Roach */ 612771ae10aSGreg Roach public function getNewXref(): string 613c1010edaSGreg Roach { 614963fbaeeSGreg Roach // Lock the row, so that only one new XREF may be generated at a time. 615963fbaeeSGreg Roach DB::table('site_setting') 616963fbaeeSGreg Roach ->where('setting_name', '=', 'next_xref') 617963fbaeeSGreg Roach ->lockForUpdate() 618963fbaeeSGreg Roach ->get(); 619963fbaeeSGreg Roach 620a214e186SGreg Roach $prefix = 'X'; 621b90d8accSGreg Roach 622971d66c8SGreg Roach $increment = 1.0; 623b90d8accSGreg Roach do { 624963fbaeeSGreg Roach $num = (int) Site::getPreference('next_xref') + (int) $increment; 625971d66c8SGreg Roach 626971d66c8SGreg Roach // This exponential increment allows us to scan over large blocks of 627971d66c8SGreg Roach // existing data in a reasonable time. 628971d66c8SGreg Roach $increment *= 1.01; 629963fbaeeSGreg Roach 630963fbaeeSGreg Roach $xref = $prefix . $num; 631963fbaeeSGreg Roach 632963fbaeeSGreg Roach // Records may already exist with this sequence number. 633963fbaeeSGreg Roach $already_used = 634963fbaeeSGreg Roach DB::table('individuals')->where('i_id', '=', $xref)->exists() || 635963fbaeeSGreg Roach DB::table('families')->where('f_id', '=', $xref)->exists() || 636963fbaeeSGreg Roach DB::table('sources')->where('s_id', '=', $xref)->exists() || 637963fbaeeSGreg Roach DB::table('media')->where('m_id', '=', $xref)->exists() || 638963fbaeeSGreg Roach DB::table('other')->where('o_id', '=', $xref)->exists() || 639963fbaeeSGreg Roach DB::table('change')->where('xref', '=', $xref)->exists(); 640963fbaeeSGreg Roach } while ($already_used); 641963fbaeeSGreg Roach 642963fbaeeSGreg Roach Site::setPreference('next_xref', (string) $num); 643b90d8accSGreg Roach 644a214e186SGreg Roach return $xref; 645b90d8accSGreg Roach } 646b90d8accSGreg Roach 647b90d8accSGreg Roach /** 648afb591d7SGreg Roach * Create a new family from GEDCOM data. 649afb591d7SGreg Roach * 650afb591d7SGreg Roach * @param string $gedcom 651afb591d7SGreg Roach * 652afb591d7SGreg Roach * @return Family 653afb591d7SGreg Roach * @throws InvalidArgumentException 654afb591d7SGreg Roach */ 655afb591d7SGreg Roach public function createFamily(string $gedcom): GedcomRecord 656afb591d7SGreg Roach { 657bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ FAM')) { 658afb591d7SGreg Roach throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM'); 659afb591d7SGreg Roach } 660afb591d7SGreg Roach 661afb591d7SGreg Roach $xref = $this->getNewXref(); 662bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ FAM' . Str::after($gedcom, '0 @@ FAM'); 663afb591d7SGreg Roach 664afb591d7SGreg Roach // Create a change record 665e5a6b4d4SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 666afb591d7SGreg Roach 667afb591d7SGreg Roach // Create a pending change 668963fbaeeSGreg Roach DB::table('change')->insert([ 669963fbaeeSGreg Roach 'gedcom_id' => $this->id, 670963fbaeeSGreg Roach 'xref' => $xref, 671963fbaeeSGreg Roach 'old_gedcom' => '', 672963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 673963fbaeeSGreg Roach 'user_id' => Auth::id(), 674afb591d7SGreg Roach ]); 675304f20d5SGreg Roach 676304f20d5SGreg Roach // Accept this pending change 677304f20d5SGreg Roach if (Auth::user()->getPreference('auto_accept')) { 678cc5684fdSGreg Roach FunctionsImport::acceptAllChanges($xref, $this); 679afb591d7SGreg Roach 680afb591d7SGreg Roach return new Family($xref, $gedcom, null, $this); 681304f20d5SGreg Roach } 682afb591d7SGreg Roach 683afb591d7SGreg Roach return new Family($xref, '', $gedcom, $this); 684afb591d7SGreg Roach } 685afb591d7SGreg Roach 686afb591d7SGreg Roach /** 687afb591d7SGreg Roach * Create a new individual from GEDCOM data. 688afb591d7SGreg Roach * 689afb591d7SGreg Roach * @param string $gedcom 690afb591d7SGreg Roach * 691afb591d7SGreg Roach * @return Individual 692afb591d7SGreg Roach * @throws InvalidArgumentException 693afb591d7SGreg Roach */ 694afb591d7SGreg Roach public function createIndividual(string $gedcom): GedcomRecord 695afb591d7SGreg Roach { 696bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ INDI')) { 697afb591d7SGreg Roach throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI'); 698afb591d7SGreg Roach } 699afb591d7SGreg Roach 700afb591d7SGreg Roach $xref = $this->getNewXref(); 701bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ INDI' . Str::after($gedcom, '0 @@ INDI'); 702afb591d7SGreg Roach 703afb591d7SGreg Roach // Create a change record 704e5a6b4d4SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 705afb591d7SGreg Roach 706afb591d7SGreg Roach // Create a pending change 707963fbaeeSGreg Roach DB::table('change')->insert([ 708963fbaeeSGreg Roach 'gedcom_id' => $this->id, 709963fbaeeSGreg Roach 'xref' => $xref, 710963fbaeeSGreg Roach 'old_gedcom' => '', 711963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 712963fbaeeSGreg Roach 'user_id' => Auth::id(), 713afb591d7SGreg Roach ]); 714afb591d7SGreg Roach 715afb591d7SGreg Roach // Accept this pending change 716afb591d7SGreg Roach if (Auth::user()->getPreference('auto_accept')) { 717afb591d7SGreg Roach FunctionsImport::acceptAllChanges($xref, $this); 718afb591d7SGreg Roach 719afb591d7SGreg Roach return new Individual($xref, $gedcom, null, $this); 720afb591d7SGreg Roach } 721afb591d7SGreg Roach 722afb591d7SGreg Roach return new Individual($xref, '', $gedcom, $this); 723304f20d5SGreg Roach } 7248586983fSGreg Roach 7258586983fSGreg Roach /** 72620b58d20SGreg Roach * Create a new media object from GEDCOM data. 72720b58d20SGreg Roach * 72820b58d20SGreg Roach * @param string $gedcom 72920b58d20SGreg Roach * 73020b58d20SGreg Roach * @return Media 73120b58d20SGreg Roach * @throws InvalidArgumentException 73220b58d20SGreg Roach */ 73320b58d20SGreg Roach public function createMediaObject(string $gedcom): Media 73420b58d20SGreg Roach { 735bec87e94SGreg Roach if (!Str::startsWith($gedcom, '0 @@ OBJE')) { 73620b58d20SGreg Roach throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE'); 73720b58d20SGreg Roach } 73820b58d20SGreg Roach 73920b58d20SGreg Roach $xref = $this->getNewXref(); 740bec87e94SGreg Roach $gedcom = '0 @' . $xref . '@ OBJE' . Str::after($gedcom, '0 @@ OBJE'); 74120b58d20SGreg Roach 74220b58d20SGreg Roach // Create a change record 743e5a6b4d4SGreg Roach $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 74420b58d20SGreg Roach 74520b58d20SGreg Roach // Create a pending change 746963fbaeeSGreg Roach DB::table('change')->insert([ 747963fbaeeSGreg Roach 'gedcom_id' => $this->id, 748963fbaeeSGreg Roach 'xref' => $xref, 749963fbaeeSGreg Roach 'old_gedcom' => '', 750963fbaeeSGreg Roach 'new_gedcom' => $gedcom, 751963fbaeeSGreg Roach 'user_id' => Auth::id(), 75220b58d20SGreg Roach ]); 75320b58d20SGreg Roach 75420b58d20SGreg Roach // Accept this pending change 75520b58d20SGreg Roach if (Auth::user()->getPreference('auto_accept')) { 75620b58d20SGreg Roach FunctionsImport::acceptAllChanges($xref, $this); 75720b58d20SGreg Roach 75820b58d20SGreg Roach return new Media($xref, $gedcom, null, $this); 75920b58d20SGreg Roach } 76020b58d20SGreg Roach 76120b58d20SGreg Roach return new Media($xref, '', $gedcom, $this); 76220b58d20SGreg Roach } 76320b58d20SGreg Roach 76420b58d20SGreg Roach /** 7658586983fSGreg Roach * What is the most significant individual in this tree. 7668586983fSGreg Roach * 767e5a6b4d4SGreg Roach * @param UserInterface $user 7688586983fSGreg Roach * 7698586983fSGreg Roach * @return Individual 7708586983fSGreg Roach */ 771e5a6b4d4SGreg Roach public function significantIndividual(UserInterface $user): Individual 772c1010edaSGreg Roach { 7738f9b0fb2SGreg Roach $individual = null; 7748586983fSGreg Roach 7758f9b0fb2SGreg Roach if ($this->getUserPreference($user, 'rootid') !== '') { 7768586983fSGreg Roach $individual = Individual::getInstance($this->getUserPreference($user, 'rootid'), $this); 7778586983fSGreg Roach } 7788f9b0fb2SGreg Roach 7798f9b0fb2SGreg Roach if ($individual === null && $this->getUserPreference($user, 'gedcomid') !== '') { 7808586983fSGreg Roach $individual = Individual::getInstance($this->getUserPreference($user, 'gedcomid'), $this); 7818586983fSGreg Roach } 7828f9b0fb2SGreg Roach 783bec87e94SGreg Roach if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') { 7848586983fSGreg Roach $individual = Individual::getInstance($this->getPreference('PEDIGREE_ROOT_ID'), $this); 7858586983fSGreg Roach } 7868f9b0fb2SGreg Roach if ($individual === null) { 7878f9b0fb2SGreg Roach $xref = (string) DB::table('individuals') 7888f9b0fb2SGreg Roach ->where('i_file', '=', $this->id()) 7898f9b0fb2SGreg Roach ->min('i_id'); 790769d7d6eSGreg Roach 791769d7d6eSGreg Roach $individual = Individual::getInstance($xref, $this); 7925fe1add5SGreg Roach } 7938f9b0fb2SGreg Roach if ($individual === null) { 7945fe1add5SGreg Roach // always return a record 7955fe1add5SGreg Roach $individual = new Individual('I', '0 @I@ INDI', null, $this); 7965fe1add5SGreg Roach } 7975fe1add5SGreg Roach 7985fe1add5SGreg Roach return $individual; 7995fe1add5SGreg Roach } 8001df7ae39SGreg Roach 80185a166d8SGreg Roach /** 80285a166d8SGreg Roach * Where do we store our media files. 80385a166d8SGreg Roach * 80485a166d8SGreg Roach * @return FilesystemInterface 80585a166d8SGreg Roach */ 8061df7ae39SGreg Roach public function mediaFilesystem(): FilesystemInterface 8071df7ae39SGreg Roach { 808456d0d35SGreg Roach $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/'); 809456d0d35SGreg Roach $filesystem = app(FilesystemInterface::class); 810456d0d35SGreg Roach $adapter = new ChrootAdapter($filesystem, $media_dir); 811456d0d35SGreg Roach 812456d0d35SGreg Roach return new Filesystem($adapter); 8131df7ae39SGreg Roach } 814a25f0a04SGreg Roach} 815