1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees; 21 22use Closure; 23use Fisharebest\Flysystem\Adapter\ChrootAdapter; 24use Fisharebest\Webtrees\Contracts\UserInterface; 25use Fisharebest\Webtrees\Functions\FunctionsExport; 26use Fisharebest\Webtrees\Functions\FunctionsImport; 27use Fisharebest\Webtrees\Services\TreeService; 28use Illuminate\Database\Capsule\Manager as DB; 29use Illuminate\Database\Query\Expression; 30use Illuminate\Support\Collection; 31use Illuminate\Support\Str; 32use InvalidArgumentException; 33use League\Flysystem\Filesystem; 34use League\Flysystem\FilesystemInterface; 35use Psr\Http\Message\StreamInterface; 36use stdClass; 37 38/** 39 * Provide an interface to the wt_gedcom table. 40 */ 41class Tree 42{ 43 private const RESN_PRIVACY = [ 44 'none' => Auth::PRIV_PRIVATE, 45 'privacy' => Auth::PRIV_USER, 46 'confidential' => Auth::PRIV_NONE, 47 'hidden' => Auth::PRIV_HIDE, 48 ]; 49 50 /** @var int The tree's ID number */ 51 private $id; 52 53 /** @var string The tree's name */ 54 private $name; 55 56 /** @var string The tree's title */ 57 private $title; 58 59 /** @var int[] Default access rules for facts in this tree */ 60 private $fact_privacy; 61 62 /** @var int[] Default access rules for individuals in this tree */ 63 private $individual_privacy; 64 65 /** @var integer[][] Default access rules for individual facts in this tree */ 66 private $individual_fact_privacy; 67 68 /** @var string[] Cached copy of the wt_gedcom_setting table. */ 69 private $preferences = []; 70 71 /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */ 72 private $user_preferences = []; 73 74 /** 75 * Create a tree object. 76 * 77 * @param int $id 78 * @param string $name 79 * @param string $title 80 */ 81 public function __construct(int $id, string $name, string $title) 82 { 83 $this->id = $id; 84 $this->name = $name; 85 $this->title = $title; 86 $this->fact_privacy = []; 87 $this->individual_privacy = []; 88 $this->individual_fact_privacy = []; 89 90 // Load the privacy settings for this tree 91 $rows = DB::table('default_resn') 92 ->where('gedcom_id', '=', $this->id) 93 ->get(); 94 95 foreach ($rows as $row) { 96 // Convert GEDCOM privacy restriction to a webtrees access level. 97 $row->resn = self::RESN_PRIVACY[$row->resn]; 98 99 if ($row->xref !== null) { 100 if ($row->tag_type !== null) { 101 $this->individual_fact_privacy[$row->xref][$row->tag_type] = (int) $row->resn; 102 } else { 103 $this->individual_privacy[$row->xref] = (int) $row->resn; 104 } 105 } else { 106 $this->fact_privacy[$row->tag_type] = (int) $row->resn; 107 } 108 } 109 } 110 111 /** 112 * A closure which will create a record from a database row. 113 * 114 * @return Closure 115 */ 116 public static function rowMapper(): Closure 117 { 118 return static function (stdClass $row): Tree { 119 return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title); 120 }; 121 } 122 123 /** 124 * Find the tree with a specific ID. 125 * 126 * @param int $tree_id 127 * 128 * @return Tree 129 */ 130 public static function findById(int $tree_id): Tree 131 { 132 return self::getAll()[$tree_id]; 133 } 134 135 /** 136 * Fetch all the trees that we have permission to access. 137 * 138 * @return Tree[] 139 * @deprecated 140 */ 141 public static function getAll(): array 142 { 143 return (new TreeService())->all()->all(); 144 } 145 146 /** 147 * All the trees that we have permission to access. 148 * 149 * @return Collection 150 * @deprecated 151 */ 152 public static function all(): Collection 153 { 154 return (new TreeService())->all(); 155 } 156 157 /** 158 * Create arguments to select_edit_control() 159 * Note - these will be escaped later 160 * 161 * @return string[] 162 */ 163 public static function getIdList(): array 164 { 165 $list = []; 166 foreach (self::getAll() as $tree) { 167 $list[$tree->id] = $tree->title; 168 } 169 170 return $list; 171 } 172 173 /** 174 * Create arguments to select_edit_control() 175 * Note - these will be escaped later 176 * 177 * @return string[] 178 */ 179 public static function getNameList(): array 180 { 181 $list = []; 182 foreach (self::getAll() as $tree) { 183 $list[$tree->name] = $tree->title; 184 } 185 186 return $list; 187 } 188 189 /** 190 * Create a new tree 191 * 192 * @param string $tree_name 193 * @param string $tree_title 194 * 195 * @return Tree 196 * @deprecated 197 */ 198 public static function create(string $tree_name, string $tree_title): Tree 199 { 200 return (new TreeService())->create($tree_name, $tree_title); 201 } 202 203 /** 204 * Find the tree with a specific name. 205 * 206 * @param string $name 207 * 208 * @return Tree|null 209 * @deprecated 210 */ 211 public static function findByName($name): ?Tree 212 { 213 foreach (self::getAll() as $tree) { 214 if ($tree->name === $name) { 215 return $tree; 216 } 217 } 218 219 return null; 220 } 221 222 /** 223 * Set the tree’s configuration settings. 224 * 225 * @param string $setting_name 226 * @param string $setting_value 227 * 228 * @return $this 229 */ 230 public function setPreference(string $setting_name, string $setting_value): Tree 231 { 232 if ($setting_value !== $this->getPreference($setting_name)) { 233 DB::table('gedcom_setting')->updateOrInsert([ 234 'gedcom_id' => $this->id, 235 'setting_name' => $setting_name, 236 ], [ 237 'setting_value' => $setting_value, 238 ]); 239 240 $this->preferences[$setting_name] = $setting_value; 241 242 Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this); 243 } 244 245 return $this; 246 } 247 248 /** 249 * Get the tree’s configuration settings. 250 * 251 * @param string $setting_name 252 * @param string $default 253 * 254 * @return string 255 */ 256 public function getPreference(string $setting_name, string $default = ''): string 257 { 258 if (empty($this->preferences)) { 259 $this->preferences = DB::table('gedcom_setting') 260 ->where('gedcom_id', '=', $this->id) 261 ->pluck('setting_value', 'setting_name') 262 ->all(); 263 } 264 265 return $this->preferences[$setting_name] ?? $default; 266 } 267 268 /** 269 * The name of this tree 270 * 271 * @return string 272 */ 273 public function name(): string 274 { 275 return $this->name; 276 } 277 278 /** 279 * The title of this tree 280 * 281 * @return string 282 */ 283 public function title(): string 284 { 285 return $this->title; 286 } 287 288 /** 289 * The fact-level privacy for this tree. 290 * 291 * @return int[] 292 */ 293 public function getFactPrivacy(): array 294 { 295 return $this->fact_privacy; 296 } 297 298 /** 299 * The individual-level privacy for this tree. 300 * 301 * @return int[] 302 */ 303 public function getIndividualPrivacy(): array 304 { 305 return $this->individual_privacy; 306 } 307 308 /** 309 * The individual-fact-level privacy for this tree. 310 * 311 * @return int[][] 312 */ 313 public function getIndividualFactPrivacy(): array 314 { 315 return $this->individual_fact_privacy; 316 } 317 318 /** 319 * Set the tree’s user-configuration settings. 320 * 321 * @param UserInterface $user 322 * @param string $setting_name 323 * @param string $setting_value 324 * 325 * @return $this 326 */ 327 public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree 328 { 329 if ($this->getUserPreference($user, $setting_name) !== $setting_value) { 330 // Update the database 331 DB::table('user_gedcom_setting')->updateOrInsert([ 332 'gedcom_id' => $this->id(), 333 'user_id' => $user->id(), 334 'setting_name' => $setting_name, 335 ], [ 336 'setting_value' => $setting_value, 337 ]); 338 339 // Update the cache 340 $this->user_preferences[$user->id()][$setting_name] = $setting_value; 341 // Audit log of changes 342 Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this); 343 } 344 345 return $this; 346 } 347 348 /** 349 * Get the tree’s user-configuration settings. 350 * 351 * @param UserInterface $user 352 * @param string $setting_name 353 * @param string $default 354 * 355 * @return string 356 */ 357 public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string 358 { 359 // There are lots of settings, and we need to fetch lots of them on every page 360 // so it is quicker to fetch them all in one go. 361 if (!array_key_exists($user->id(), $this->user_preferences)) { 362 $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting') 363 ->where('user_id', '=', $user->id()) 364 ->where('gedcom_id', '=', $this->id) 365 ->pluck('setting_value', 'setting_name') 366 ->all(); 367 } 368 369 return $this->user_preferences[$user->id()][$setting_name] ?? $default; 370 } 371 372 /** 373 * The ID of this tree 374 * 375 * @return int 376 */ 377 public function id(): int 378 { 379 return $this->id; 380 } 381 382 /** 383 * Can a user accept changes for this tree? 384 * 385 * @param UserInterface $user 386 * 387 * @return bool 388 */ 389 public function canAcceptChanges(UserInterface $user): bool 390 { 391 return Auth::isModerator($this, $user); 392 } 393 394 /** 395 * Are there any pending edits for this tree, than need reviewing by a moderator. 396 * 397 * @return bool 398 */ 399 public function hasPendingEdit(): bool 400 { 401 return DB::table('change') 402 ->where('gedcom_id', '=', $this->id) 403 ->where('status', '=', 'pending') 404 ->exists(); 405 } 406 407 /** 408 * Delete everything relating to a tree 409 * 410 * @return void 411 */ 412 public function delete(): void 413 { 414 // If this is the default tree, then unset it 415 if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) { 416 Site::setPreference('DEFAULT_GEDCOM', ''); 417 } 418 419 $this->deleteGenealogyData(false); 420 421 DB::table('block_setting') 422 ->join('block', 'block.block_id', '=', 'block_setting.block_id') 423 ->where('gedcom_id', '=', $this->id) 424 ->delete(); 425 DB::table('block')->where('gedcom_id', '=', $this->id)->delete(); 426 DB::table('user_gedcom_setting')->where('gedcom_id', '=', $this->id)->delete(); 427 DB::table('gedcom_setting')->where('gedcom_id', '=', $this->id)->delete(); 428 DB::table('module_privacy')->where('gedcom_id', '=', $this->id)->delete(); 429 DB::table('hit_counter')->where('gedcom_id', '=', $this->id)->delete(); 430 DB::table('default_resn')->where('gedcom_id', '=', $this->id)->delete(); 431 DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete(); 432 DB::table('log')->where('gedcom_id', '=', $this->id)->delete(); 433 DB::table('gedcom')->where('gedcom_id', '=', $this->id)->delete(); 434 } 435 436 /** 437 * Delete all the genealogy data from a tree - in preparation for importing 438 * new data. Optionally retain the media data, for when the user has been 439 * editing their data offline using an application which deletes (or does not 440 * support) media data. 441 * 442 * @param bool $keep_media 443 * 444 * @return void 445 */ 446 public function deleteGenealogyData(bool $keep_media): void 447 { 448 DB::table('gedcom_chunk')->where('gedcom_id', '=', $this->id)->delete(); 449 DB::table('individuals')->where('i_file', '=', $this->id)->delete(); 450 DB::table('families')->where('f_file', '=', $this->id)->delete(); 451 DB::table('sources')->where('s_file', '=', $this->id)->delete(); 452 DB::table('other')->where('o_file', '=', $this->id)->delete(); 453 DB::table('places')->where('p_file', '=', $this->id)->delete(); 454 DB::table('placelinks')->where('pl_file', '=', $this->id)->delete(); 455 DB::table('name')->where('n_file', '=', $this->id)->delete(); 456 DB::table('dates')->where('d_file', '=', $this->id)->delete(); 457 DB::table('change')->where('gedcom_id', '=', $this->id)->delete(); 458 459 if ($keep_media) { 460 DB::table('link')->where('l_file', '=', $this->id) 461 ->where('l_type', '<>', 'OBJE') 462 ->delete(); 463 } else { 464 DB::table('link')->where('l_file', '=', $this->id)->delete(); 465 DB::table('media_file')->where('m_file', '=', $this->id)->delete(); 466 DB::table('media')->where('m_file', '=', $this->id)->delete(); 467 } 468 } 469 470 /** 471 * Export the tree to a GEDCOM file 472 * 473 * @param resource $stream 474 * 475 * @return void 476 */ 477 public function exportGedcom($stream): void 478 { 479 $buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this, 'UTF-8')); 480 481 $union_families = DB::table('families') 482 ->where('f_file', '=', $this->id) 483 ->select(['f_gedcom AS gedcom', 'f_id AS xref', new Expression('LENGTH(f_id) AS len'), new Expression('2 AS n')]); 484 485 $union_sources = DB::table('sources') 486 ->where('s_file', '=', $this->id) 487 ->select(['s_gedcom AS gedcom', 's_id AS xref', new Expression('LENGTH(s_id) AS len'), new Expression('3 AS n')]); 488 489 $union_other = DB::table('other') 490 ->where('o_file', '=', $this->id) 491 ->whereNotIn('o_type', ['HEAD', 'TRLR']) 492 ->select(['o_gedcom AS gedcom', 'o_id AS xref', new Expression('LENGTH(o_id) AS len'), new Expression('4 AS n')]); 493 494 $union_media = DB::table('media') 495 ->where('m_file', '=', $this->id) 496 ->select(['m_gedcom AS gedcom', 'm_id AS xref', new Expression('LENGTH(m_id) AS len'), new Expression('5 AS n')]); 497 498 DB::table('individuals') 499 ->where('i_file', '=', $this->id) 500 ->select(['i_gedcom AS gedcom', 'i_id AS xref', new Expression('LENGTH(i_id) AS len'), new Expression('1 AS n')]) 501 ->union($union_families) 502 ->union($union_sources) 503 ->union($union_other) 504 ->union($union_media) 505 ->orderBy('n') 506 ->orderBy('len') 507 ->orderBy('xref') 508 ->chunk(1000, static function (Collection $rows) use ($stream, &$buffer): void { 509 foreach ($rows as $row) { 510 $buffer .= FunctionsExport::reformatRecord($row->gedcom); 511 if (strlen($buffer) > 65535) { 512 fwrite($stream, $buffer); 513 $buffer = ''; 514 } 515 } 516 }); 517 518 fwrite($stream, $buffer . '0 TRLR' . Gedcom::EOL); 519 } 520 521 /** 522 * Import data from a gedcom file into this tree. 523 * 524 * @param StreamInterface $stream The GEDCOM file. 525 * @param string $filename The preferred filename, for export/download. 526 * 527 * @return void 528 */ 529 public function importGedcomFile(StreamInterface $stream, string $filename): void 530 { 531 // Read the file in blocks of roughly 64K. Ensure that each block 532 // contains complete gedcom records. This will ensure we don’t split 533 // multi-byte characters, as well as simplifying the code to import 534 // each block. 535 536 $file_data = ''; 537 538 $this->deleteGenealogyData((bool) $this->getPreference('keep_media')); 539 $this->setPreference('gedcom_filename', $filename); 540 $this->setPreference('imported', '0'); 541 542 while (!$stream->eof()) { 543 $file_data .= $stream->read(65536); 544 // There is no strrpos() function that searches for substrings :-( 545 for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) { 546 if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) { 547 // We’ve found the last record boundary in this chunk of data 548 break; 549 } 550 } 551 if ($pos) { 552 DB::table('gedcom_chunk')->insert([ 553 'gedcom_id' => $this->id, 554 'chunk_data' => substr($file_data, 0, $pos), 555 ]); 556 557 $file_data = substr($file_data, $pos); 558 } 559 } 560 DB::table('gedcom_chunk')->insert([ 561 'gedcom_id' => $this->id, 562 'chunk_data' => $file_data, 563 ]); 564 565 $stream->close(); 566 } 567 568 /** 569 * Create a new record from GEDCOM data. 570 * 571 * @param string $gedcom 572 * 573 * @return GedcomRecord|Individual|Family|Note|Source|Repository|Media 574 * @throws InvalidArgumentException 575 */ 576 public function createRecord(string $gedcom): GedcomRecord 577 { 578 if (!Str::startsWith($gedcom, '0 @@ ')) { 579 throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@'); 580 } 581 582 $xref = $this->getNewXref(); 583 $gedcom = '0 @' . $xref . '@ ' . Str::after($gedcom, '0 @@ '); 584 585 // Create a change record 586 $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 587 588 // Create a pending change 589 DB::table('change')->insert([ 590 'gedcom_id' => $this->id, 591 'xref' => $xref, 592 'old_gedcom' => '', 593 'new_gedcom' => $gedcom, 594 'user_id' => Auth::id(), 595 ]); 596 597 // Accept this pending change 598 if (Auth::user()->getPreference('auto_accept')) { 599 FunctionsImport::acceptAllChanges($xref, $this); 600 601 return new GedcomRecord($xref, $gedcom, null, $this); 602 } 603 604 return GedcomRecord::getInstance($xref, $this, $gedcom); 605 } 606 607 /** 608 * Generate a new XREF, unique across all family trees 609 * 610 * @return string 611 */ 612 public function getNewXref(): string 613 { 614 // Lock the row, so that only one new XREF may be generated at a time. 615 DB::table('site_setting') 616 ->where('setting_name', '=', 'next_xref') 617 ->lockForUpdate() 618 ->get(); 619 620 $prefix = 'X'; 621 622 $increment = 1.0; 623 do { 624 $num = (int) Site::getPreference('next_xref') + (int) $increment; 625 626 // This exponential increment allows us to scan over large blocks of 627 // existing data in a reasonable time. 628 $increment *= 1.01; 629 630 $xref = $prefix . $num; 631 632 // Records may already exist with this sequence number. 633 $already_used = 634 DB::table('individuals')->where('i_id', '=', $xref)->exists() || 635 DB::table('families')->where('f_id', '=', $xref)->exists() || 636 DB::table('sources')->where('s_id', '=', $xref)->exists() || 637 DB::table('media')->where('m_id', '=', $xref)->exists() || 638 DB::table('other')->where('o_id', '=', $xref)->exists() || 639 DB::table('change')->where('xref', '=', $xref)->exists(); 640 } while ($already_used); 641 642 Site::setPreference('next_xref', (string) $num); 643 644 return $xref; 645 } 646 647 /** 648 * Create a new family from GEDCOM data. 649 * 650 * @param string $gedcom 651 * 652 * @return Family 653 * @throws InvalidArgumentException 654 */ 655 public function createFamily(string $gedcom): GedcomRecord 656 { 657 if (!Str::startsWith($gedcom, '0 @@ FAM')) { 658 throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM'); 659 } 660 661 $xref = $this->getNewXref(); 662 $gedcom = '0 @' . $xref . '@ FAM' . Str::after($gedcom, '0 @@ FAM'); 663 664 // Create a change record 665 $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 666 667 // Create a pending change 668 DB::table('change')->insert([ 669 'gedcom_id' => $this->id, 670 'xref' => $xref, 671 'old_gedcom' => '', 672 'new_gedcom' => $gedcom, 673 'user_id' => Auth::id(), 674 ]); 675 676 // Accept this pending change 677 if (Auth::user()->getPreference('auto_accept')) { 678 FunctionsImport::acceptAllChanges($xref, $this); 679 680 return new Family($xref, $gedcom, null, $this); 681 } 682 683 return new Family($xref, '', $gedcom, $this); 684 } 685 686 /** 687 * Create a new individual from GEDCOM data. 688 * 689 * @param string $gedcom 690 * 691 * @return Individual 692 * @throws InvalidArgumentException 693 */ 694 public function createIndividual(string $gedcom): GedcomRecord 695 { 696 if (!Str::startsWith($gedcom, '0 @@ INDI')) { 697 throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI'); 698 } 699 700 $xref = $this->getNewXref(); 701 $gedcom = '0 @' . $xref . '@ INDI' . Str::after($gedcom, '0 @@ INDI'); 702 703 // Create a change record 704 $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 705 706 // Create a pending change 707 DB::table('change')->insert([ 708 'gedcom_id' => $this->id, 709 'xref' => $xref, 710 'old_gedcom' => '', 711 'new_gedcom' => $gedcom, 712 'user_id' => Auth::id(), 713 ]); 714 715 // Accept this pending change 716 if (Auth::user()->getPreference('auto_accept')) { 717 FunctionsImport::acceptAllChanges($xref, $this); 718 719 return new Individual($xref, $gedcom, null, $this); 720 } 721 722 return new Individual($xref, '', $gedcom, $this); 723 } 724 725 /** 726 * Create a new media object from GEDCOM data. 727 * 728 * @param string $gedcom 729 * 730 * @return Media 731 * @throws InvalidArgumentException 732 */ 733 public function createMediaObject(string $gedcom): Media 734 { 735 if (!Str::startsWith($gedcom, '0 @@ OBJE')) { 736 throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE'); 737 } 738 739 $xref = $this->getNewXref(); 740 $gedcom = '0 @' . $xref . '@ OBJE' . Str::after($gedcom, '0 @@ OBJE'); 741 742 // Create a change record 743 $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->userName(); 744 745 // Create a pending change 746 DB::table('change')->insert([ 747 'gedcom_id' => $this->id, 748 'xref' => $xref, 749 'old_gedcom' => '', 750 'new_gedcom' => $gedcom, 751 'user_id' => Auth::id(), 752 ]); 753 754 // Accept this pending change 755 if (Auth::user()->getPreference('auto_accept')) { 756 FunctionsImport::acceptAllChanges($xref, $this); 757 758 return new Media($xref, $gedcom, null, $this); 759 } 760 761 return new Media($xref, '', $gedcom, $this); 762 } 763 764 /** 765 * What is the most significant individual in this tree. 766 * 767 * @param UserInterface $user 768 * 769 * @return Individual 770 */ 771 public function significantIndividual(UserInterface $user): Individual 772 { 773 $individual = null; 774 775 if ($this->getUserPreference($user, 'rootid') !== '') { 776 $individual = Individual::getInstance($this->getUserPreference($user, 'rootid'), $this); 777 } 778 779 if ($individual === null && $this->getUserPreference($user, 'gedcomid') !== '') { 780 $individual = Individual::getInstance($this->getUserPreference($user, 'gedcomid'), $this); 781 } 782 783 if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') { 784 $individual = Individual::getInstance($this->getPreference('PEDIGREE_ROOT_ID'), $this); 785 } 786 if ($individual === null) { 787 $xref = (string) DB::table('individuals') 788 ->where('i_file', '=', $this->id()) 789 ->min('i_id'); 790 791 $individual = Individual::getInstance($xref, $this); 792 } 793 if ($individual === null) { 794 // always return a record 795 $individual = new Individual('I', '0 @I@ INDI', null, $this); 796 } 797 798 return $individual; 799 } 800 801 /** 802 * Where do we store our media files. 803 * 804 * @return FilesystemInterface 805 */ 806 public function mediaFilesystem(): FilesystemInterface 807 { 808 $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/'); 809 $filesystem = app(FilesystemInterface::class); 810 $adapter = new ChrootAdapter($filesystem, $media_dir); 811 812 return new Filesystem($adapter); 813 } 814} 815