1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 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 <https://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\Services\PendingChangesService; 26use Illuminate\Database\Capsule\Manager as DB; 27use InvalidArgumentException; 28use League\Flysystem\Filesystem; 29use League\Flysystem\FilesystemOperator; 30use stdClass; 31 32use function app; 33use function array_key_exists; 34use function date; 35use function str_starts_with; 36use function strtoupper; 37use function substr_replace; 38 39/** 40 * Provide an interface to the wt_gedcom table. 41 */ 42class Tree 43{ 44 private const RESN_PRIVACY = [ 45 'none' => Auth::PRIV_PRIVATE, 46 'privacy' => Auth::PRIV_USER, 47 'confidential' => Auth::PRIV_NONE, 48 'hidden' => Auth::PRIV_HIDE, 49 ]; 50 51 /** @var int The tree's ID number */ 52 private $id; 53 54 /** @var string The tree's name */ 55 private $name; 56 57 /** @var string The tree's title */ 58 private $title; 59 60 /** @var int[] Default access rules for facts in this tree */ 61 private $fact_privacy; 62 63 /** @var int[] Default access rules for individuals in this tree */ 64 private $individual_privacy; 65 66 /** @var integer[][] Default access rules for individual facts in this tree */ 67 private $individual_fact_privacy; 68 69 /** @var string[] Cached copy of the wt_gedcom_setting table. */ 70 private $preferences = []; 71 72 /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */ 73 private $user_preferences = []; 74 75 /** 76 * Create a tree object. 77 * 78 * @param int $id 79 * @param string $name 80 * @param string $title 81 */ 82 public function __construct(int $id, string $name, string $title) 83 { 84 $this->id = $id; 85 $this->name = $name; 86 $this->title = $title; 87 $this->fact_privacy = []; 88 $this->individual_privacy = []; 89 $this->individual_fact_privacy = []; 90 91 // Load the privacy settings for this tree 92 $rows = DB::table('default_resn') 93 ->where('gedcom_id', '=', $this->id) 94 ->get(); 95 96 foreach ($rows as $row) { 97 // Convert GEDCOM privacy restriction to a webtrees access level. 98 $row->resn = self::RESN_PRIVACY[$row->resn]; 99 100 if ($row->xref !== null) { 101 if ($row->tag_type !== null) { 102 $this->individual_fact_privacy[$row->xref][$row->tag_type] = $row->resn; 103 } else { 104 $this->individual_privacy[$row->xref] = $row->resn; 105 } 106 } else { 107 $this->fact_privacy[$row->tag_type] = $row->resn; 108 } 109 } 110 } 111 112 /** 113 * A closure which will create a record from a database row. 114 * 115 * @return Closure 116 */ 117 public static function rowMapper(): Closure 118 { 119 return static function (stdClass $row): Tree { 120 return new Tree((int) $row->tree_id, $row->tree_name, $row->tree_title); 121 }; 122 } 123 124 /** 125 * Set the tree’s configuration settings. 126 * 127 * @param string $setting_name 128 * @param string $setting_value 129 * 130 * @return $this 131 */ 132 public function setPreference(string $setting_name, string $setting_value): Tree 133 { 134 if ($setting_value !== $this->getPreference($setting_name)) { 135 DB::table('gedcom_setting')->updateOrInsert([ 136 'gedcom_id' => $this->id, 137 'setting_name' => $setting_name, 138 ], [ 139 'setting_value' => $setting_value, 140 ]); 141 142 $this->preferences[$setting_name] = $setting_value; 143 144 Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this); 145 } 146 147 return $this; 148 } 149 150 /** 151 * Get the tree’s configuration settings. 152 * 153 * @param string $setting_name 154 * @param string $default 155 * 156 * @return string 157 */ 158 public function getPreference(string $setting_name, string $default = ''): string 159 { 160 if ($this->preferences === []) { 161 $this->preferences = DB::table('gedcom_setting') 162 ->where('gedcom_id', '=', $this->id) 163 ->pluck('setting_value', 'setting_name') 164 ->all(); 165 } 166 167 return $this->preferences[$setting_name] ?? $default; 168 } 169 170 /** 171 * The name of this tree 172 * 173 * @return string 174 */ 175 public function name(): string 176 { 177 return $this->name; 178 } 179 180 /** 181 * The title of this tree 182 * 183 * @return string 184 */ 185 public function title(): string 186 { 187 return $this->title; 188 } 189 190 /** 191 * The fact-level privacy for this tree. 192 * 193 * @return int[] 194 */ 195 public function getFactPrivacy(): array 196 { 197 return $this->fact_privacy; 198 } 199 200 /** 201 * The individual-level privacy for this tree. 202 * 203 * @return int[] 204 */ 205 public function getIndividualPrivacy(): array 206 { 207 return $this->individual_privacy; 208 } 209 210 /** 211 * The individual-fact-level privacy for this tree. 212 * 213 * @return int[][] 214 */ 215 public function getIndividualFactPrivacy(): array 216 { 217 return $this->individual_fact_privacy; 218 } 219 220 /** 221 * Set the tree’s user-configuration settings. 222 * 223 * @param UserInterface $user 224 * @param string $setting_name 225 * @param string $setting_value 226 * 227 * @return $this 228 */ 229 public function setUserPreference(UserInterface $user, string $setting_name, string $setting_value): Tree 230 { 231 if ($this->getUserPreference($user, $setting_name) !== $setting_value) { 232 // Update the database 233 DB::table('user_gedcom_setting')->updateOrInsert([ 234 'gedcom_id' => $this->id(), 235 'user_id' => $user->id(), 236 'setting_name' => $setting_name, 237 ], [ 238 'setting_value' => $setting_value, 239 ]); 240 241 // Update the cache 242 $this->user_preferences[$user->id()][$setting_name] = $setting_value; 243 // Audit log of changes 244 Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->userName() . '"', $this); 245 } 246 247 return $this; 248 } 249 250 /** 251 * Get the tree’s user-configuration settings. 252 * 253 * @param UserInterface $user 254 * @param string $setting_name 255 * @param string $default 256 * 257 * @return string 258 */ 259 public function getUserPreference(UserInterface $user, string $setting_name, string $default = ''): string 260 { 261 // There are lots of settings, and we need to fetch lots of them on every page 262 // so it is quicker to fetch them all in one go. 263 if (!array_key_exists($user->id(), $this->user_preferences)) { 264 $this->user_preferences[$user->id()] = DB::table('user_gedcom_setting') 265 ->where('user_id', '=', $user->id()) 266 ->where('gedcom_id', '=', $this->id) 267 ->pluck('setting_value', 'setting_name') 268 ->all(); 269 } 270 271 return $this->user_preferences[$user->id()][$setting_name] ?? $default; 272 } 273 274 /** 275 * The ID of this tree 276 * 277 * @return int 278 */ 279 public function id(): int 280 { 281 return $this->id; 282 } 283 284 /** 285 * Can a user accept changes for this tree? 286 * 287 * @param UserInterface $user 288 * 289 * @return bool 290 */ 291 public function canAcceptChanges(UserInterface $user): bool 292 { 293 return Auth::isModerator($this, $user); 294 } 295 296 /** 297 * Are there any pending edits for this tree, than need reviewing by a moderator. 298 * 299 * @return bool 300 */ 301 public function hasPendingEdit(): bool 302 { 303 return DB::table('change') 304 ->where('gedcom_id', '=', $this->id) 305 ->where('status', '=', 'pending') 306 ->exists(); 307 } 308 309 /** 310 * Create a new record from GEDCOM data. 311 * 312 * @param string $gedcom 313 * 314 * @return GedcomRecord|Individual|Family|Location|Note|Source|Repository|Media|Submitter|Submission 315 * @throws InvalidArgumentException 316 */ 317 public function createRecord(string $gedcom): GedcomRecord 318 { 319 if (!preg_match('/^0 @@ ([_A-Z]+)/', $gedcom, $match)) { 320 throw new InvalidArgumentException('GedcomRecord::createRecord(' . $gedcom . ') does not begin 0 @@'); 321 } 322 323 $xref = Registry::xrefFactory()->make($match[1]); 324 $gedcom = substr_replace($gedcom, $xref, 3, 0); 325 326 // Create a change record 327 $today = strtoupper(date('d M Y')); 328 $now = date('H:i:s'); 329 $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName(); 330 331 // Create a pending change 332 DB::table('change')->insert([ 333 'gedcom_id' => $this->id, 334 'xref' => $xref, 335 'old_gedcom' => '', 336 'new_gedcom' => $gedcom, 337 'user_id' => Auth::id(), 338 ]); 339 340 // Accept this pending change 341 if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') { 342 $record = Registry::gedcomRecordFactory()->new($xref, $gedcom, null, $this); 343 344 app(PendingChangesService::class)->acceptRecord($record); 345 346 return $record; 347 } 348 349 return Registry::gedcomRecordFactory()->new($xref, '', $gedcom, $this); 350 } 351 352 /** 353 * Create a new family from GEDCOM data. 354 * 355 * @param string $gedcom 356 * 357 * @return Family 358 * @throws InvalidArgumentException 359 */ 360 public function createFamily(string $gedcom): GedcomRecord 361 { 362 if (!str_starts_with($gedcom, '0 @@ FAM')) { 363 throw new InvalidArgumentException('GedcomRecord::createFamily(' . $gedcom . ') does not begin 0 @@ FAM'); 364 } 365 366 $xref = Registry::xrefFactory()->make(Family::RECORD_TYPE); 367 $gedcom = substr_replace($gedcom, $xref, 3, 0); 368 369 // Create a change record 370 $today = strtoupper(date('d M Y')); 371 $now = date('H:i:s'); 372 $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName(); 373 374 // Create a pending change 375 DB::table('change')->insert([ 376 'gedcom_id' => $this->id, 377 'xref' => $xref, 378 'old_gedcom' => '', 379 'new_gedcom' => $gedcom, 380 'user_id' => Auth::id(), 381 ]); 382 383 // Accept this pending change 384 if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') { 385 $record = Registry::familyFactory()->new($xref, $gedcom, null, $this); 386 387 app(PendingChangesService::class)->acceptRecord($record); 388 389 return $record; 390 } 391 392 return Registry::familyFactory()->new($xref, '', $gedcom, $this); 393 } 394 395 /** 396 * Create a new individual from GEDCOM data. 397 * 398 * @param string $gedcom 399 * 400 * @return Individual 401 * @throws InvalidArgumentException 402 */ 403 public function createIndividual(string $gedcom): GedcomRecord 404 { 405 if (!str_starts_with($gedcom, '0 @@ INDI')) { 406 throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ INDI'); 407 } 408 409 $xref = Registry::xrefFactory()->make(Individual::RECORD_TYPE); 410 $gedcom = substr_replace($gedcom, $xref, 3, 0); 411 412 // Create a change record 413 $today = strtoupper(date('d M Y')); 414 $now = date('H:i:s'); 415 $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName(); 416 417 // Create a pending change 418 DB::table('change')->insert([ 419 'gedcom_id' => $this->id, 420 'xref' => $xref, 421 'old_gedcom' => '', 422 'new_gedcom' => $gedcom, 423 'user_id' => Auth::id(), 424 ]); 425 426 // Accept this pending change 427 if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') { 428 $record = Registry::individualFactory()->new($xref, $gedcom, null, $this); 429 430 app(PendingChangesService::class)->acceptRecord($record); 431 432 return $record; 433 } 434 435 return Registry::individualFactory()->new($xref, '', $gedcom, $this); 436 } 437 438 /** 439 * Create a new media object from GEDCOM data. 440 * 441 * @param string $gedcom 442 * 443 * @return Media 444 * @throws InvalidArgumentException 445 */ 446 public function createMediaObject(string $gedcom): Media 447 { 448 if (!str_starts_with($gedcom, '0 @@ OBJE')) { 449 throw new InvalidArgumentException('GedcomRecord::createIndividual(' . $gedcom . ') does not begin 0 @@ OBJE'); 450 } 451 452 $xref = Registry::xrefFactory()->make(Media::RECORD_TYPE); 453 $gedcom = substr_replace($gedcom, $xref, 3, 0); 454 455 // Create a change record 456 $today = strtoupper(date('d M Y')); 457 $now = date('H:i:s'); 458 $gedcom .= "\n1 CHAN\n2 DATE " . $today . "\n3 TIME " . $now . "\n2 _WT_USER " . Auth::user()->userName(); 459 460 // Create a pending change 461 DB::table('change')->insert([ 462 'gedcom_id' => $this->id, 463 'xref' => $xref, 464 'old_gedcom' => '', 465 'new_gedcom' => $gedcom, 466 'user_id' => Auth::id(), 467 ]); 468 469 // Accept this pending change 470 if (Auth::user()->getPreference(UserInterface::PREF_AUTO_ACCEPT_EDITS) === '1') { 471 $record = Registry::mediaFactory()->new($xref, $gedcom, null, $this); 472 473 app(PendingChangesService::class)->acceptRecord($record); 474 475 return $record; 476 } 477 478 return Registry::mediaFactory()->new($xref, '', $gedcom, $this); 479 } 480 481 /** 482 * What is the most significant individual in this tree. 483 * 484 * @param UserInterface $user 485 * @param string $xref 486 * 487 * @return Individual 488 */ 489 public function significantIndividual(UserInterface $user, $xref = ''): Individual 490 { 491 if ($xref === '') { 492 $individual = null; 493 } else { 494 $individual = Registry::individualFactory()->make($xref, $this); 495 496 if ($individual === null) { 497 $family = Registry::familyFactory()->make($xref, $this); 498 499 if ($family instanceof Family) { 500 $individual = $family->spouses()->first() ?? $family->children()->first(); 501 } 502 } 503 } 504 505 if ($individual === null && $this->getUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF) !== '') { 506 $individual = Registry::individualFactory()->make($this->getUserPreference($user, UserInterface::PREF_TREE_DEFAULT_XREF), $this); 507 } 508 509 if ($individual === null && $this->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF) !== '') { 510 $individual = Registry::individualFactory()->make($this->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF), $this); 511 } 512 513 if ($individual === null && $this->getPreference('PEDIGREE_ROOT_ID') !== '') { 514 $individual = Registry::individualFactory()->make($this->getPreference('PEDIGREE_ROOT_ID'), $this); 515 } 516 if ($individual === null) { 517 $xref = (string) DB::table('individuals') 518 ->where('i_file', '=', $this->id()) 519 ->min('i_id'); 520 521 $individual = Registry::individualFactory()->make($xref, $this); 522 } 523 if ($individual === null) { 524 // always return a record 525 $individual = Registry::individualFactory()->new('I', '0 @I@ INDI', null, $this); 526 } 527 528 return $individual; 529 } 530 531 /** 532 * Where do we store our media files. 533 * 534 * @param FilesystemOperator $data_filesystem 535 * 536 * @return FilesystemOperator 537 */ 538 public function mediaFilesystem(FilesystemOperator $data_filesystem): FilesystemOperator 539 { 540 $media_dir = $this->getPreference('MEDIA_DIRECTORY', 'media/'); 541 $adapter = new ChrootAdapter($data_filesystem, $media_dir); 542 543 return new Filesystem($adapter); 544 } 545} 546