1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2018 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16namespace Fisharebest\Webtrees; 17 18use Fisharebest\Webtrees\Functions\FunctionsExport; 19use Fisharebest\Webtrees\Functions\FunctionsImport; 20use PDOException; 21 22/** 23 * Provide an interface to the wt_gedcom table. 24 */ 25class Tree 26{ 27 /** @var int The tree's ID number */ 28 private $tree_id; 29 30 /** @var string The tree's name */ 31 private $name; 32 33 /** @var string The tree's title */ 34 private $title; 35 36 /** @var int[] Default access rules for facts in this tree */ 37 private $fact_privacy; 38 39 /** @var int[] Default access rules for individuals in this tree */ 40 private $individual_privacy; 41 42 /** @var integer[][] Default access rules for individual facts in this tree */ 43 private $individual_fact_privacy; 44 45 /** @var Tree[] All trees that we have permission to see. */ 46 private static $trees; 47 48 /** @var string[] Cached copy of the wt_gedcom_setting table. */ 49 private $preferences = []; 50 51 /** @var string[][] Cached copy of the wt_user_gedcom_setting table. */ 52 private $user_preferences = []; 53 54 /** 55 * Create a tree object. This is a private constructor - it can only 56 * be called from Tree::getAll() to ensure proper initialisation. 57 * 58 * @param int $tree_id 59 * @param string $tree_name 60 * @param string $tree_title 61 */ 62 private function __construct($tree_id, $tree_name, $tree_title) 63 { 64 $this->tree_id = $tree_id; 65 $this->name = $tree_name; 66 $this->title = $tree_title; 67 $this->fact_privacy = []; 68 $this->individual_privacy = []; 69 $this->individual_fact_privacy = []; 70 71 // Load the privacy settings for this tree 72 $rows = Database::prepare( 73 "SELECT xref, tag_type, CASE resn WHEN 'none' THEN :priv_public WHEN 'privacy' THEN :priv_user WHEN 'confidential' THEN :priv_none WHEN 'hidden' THEN :priv_hide END AS resn" . 74 " FROM `##default_resn` WHERE gedcom_id = :tree_id" 75 )->execute([ 76 'priv_public' => Auth::PRIV_PRIVATE, 77 'priv_user' => Auth::PRIV_USER, 78 'priv_none' => Auth::PRIV_NONE, 79 'priv_hide' => Auth::PRIV_HIDE, 80 'tree_id' => $this->tree_id, 81 ])->fetchAll(); 82 83 foreach ($rows as $row) { 84 if ($row->xref !== null) { 85 if ($row->tag_type !== null) { 86 $this->individual_fact_privacy[$row->xref][$row->tag_type] = (int)$row->resn; 87 } else { 88 $this->individual_privacy[$row->xref] = (int)$row->resn; 89 } 90 } else { 91 $this->fact_privacy[$row->tag_type] = (int)$row->resn; 92 } 93 } 94 } 95 96 /** 97 * The ID of this tree 98 * 99 * @return int 100 */ 101 public function getTreeId() 102 { 103 return $this->tree_id; 104 } 105 106 /** 107 * The name of this tree 108 * 109 * @return string 110 */ 111 public function getName() 112 { 113 return $this->name; 114 } 115 116 /** 117 * The title of this tree 118 * 119 * @return string 120 */ 121 public function getTitle() 122 { 123 return $this->title; 124 } 125 126 /** 127 * The fact-level privacy for this tree. 128 * 129 * @return int[] 130 */ 131 public function getFactPrivacy() 132 { 133 return $this->fact_privacy; 134 } 135 136 /** 137 * The individual-level privacy for this tree. 138 * 139 * @return int[] 140 */ 141 public function getIndividualPrivacy() 142 { 143 return $this->individual_privacy; 144 } 145 146 /** 147 * The individual-fact-level privacy for this tree. 148 * 149 * @return int[][] 150 */ 151 public function getIndividualFactPrivacy() 152 { 153 return $this->individual_fact_privacy; 154 } 155 156 /** 157 * Get the tree’s configuration settings. 158 * 159 * @param string $setting_name 160 * @param string $default 161 * 162 * @return string 163 */ 164 public function getPreference($setting_name, $default = '') 165 { 166 if (empty($this->preferences)) { 167 $this->preferences = Database::prepare( 168 "SELECT setting_name, setting_value FROM `##gedcom_setting` WHERE gedcom_id = ?" 169 )->execute([$this->tree_id])->fetchAssoc(); 170 } 171 172 if (array_key_exists($setting_name, $this->preferences)) { 173 return $this->preferences[$setting_name]; 174 } else { 175 return $default; 176 } 177 } 178 179 /** 180 * Set the tree’s configuration settings. 181 * 182 * @param string $setting_name 183 * @param string $setting_value 184 * 185 * @return $this 186 */ 187 public function setPreference($setting_name, $setting_value) 188 { 189 if ($setting_value !== $this->getPreference($setting_name)) { 190 Database::prepare( 191 "REPLACE INTO `##gedcom_setting` (gedcom_id, setting_name, setting_value)" . 192 " VALUES (:tree_id, :setting_name, LEFT(:setting_value, 255))" 193 )->execute([ 194 'tree_id' => $this->tree_id, 195 'setting_name' => $setting_name, 196 'setting_value' => $setting_value, 197 ]); 198 199 $this->preferences[$setting_name] = $setting_value; 200 201 Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this); 202 } 203 204 return $this; 205 } 206 207 /** 208 * Get the tree’s user-configuration settings. 209 * 210 * @param User $user 211 * @param string $setting_name 212 * @param string|null $default 213 * 214 * @return string 215 */ 216 public function getUserPreference(User $user, $setting_name, $default = null) 217 { 218 // There are lots of settings, and we need to fetch lots of them on every page 219 // so it is quicker to fetch them all in one go. 220 if (!array_key_exists($user->getUserId(), $this->user_preferences)) { 221 $this->user_preferences[$user->getUserId()] = Database::prepare( 222 "SELECT setting_name, setting_value FROM `##user_gedcom_setting` WHERE user_id = ? AND gedcom_id = ?" 223 )->execute([ 224 $user->getUserId(), 225 $this->tree_id, 226 ])->fetchAssoc(); 227 } 228 229 if (array_key_exists($setting_name, $this->user_preferences[$user->getUserId()])) { 230 return $this->user_preferences[$user->getUserId()][$setting_name]; 231 } else { 232 return $default; 233 } 234 } 235 236 /** 237 * Set the tree’s user-configuration settings. 238 * 239 * @param User $user 240 * @param string $setting_name 241 * @param string $setting_value 242 * 243 * @return $this 244 */ 245 public function setUserPreference(User $user, $setting_name, $setting_value) 246 { 247 if ($this->getUserPreference($user, $setting_name) !== $setting_value) { 248 // Update the database 249 if ($setting_value === null) { 250 Database::prepare( 251 "DELETE FROM `##user_gedcom_setting` WHERE gedcom_id = :tree_id AND user_id = :user_id AND setting_name = :setting_name" 252 )->execute([ 253 'tree_id' => $this->tree_id, 254 'user_id' => $user->getUserId(), 255 'setting_name' => $setting_name, 256 ]); 257 } else { 258 Database::prepare( 259 "REPLACE INTO `##user_gedcom_setting` (user_id, gedcom_id, setting_name, setting_value) VALUES (:user_id, :tree_id, :setting_name, LEFT(:setting_value, 255))" 260 )->execute([ 261 'user_id' => $user->getUserId(), 262 'tree_id' => $this->tree_id, 263 'setting_name' => $setting_name, 264 'setting_value' => $setting_value, 265 ]); 266 } 267 // Update our cache 268 $this->user_preferences[$user->getUserId()][$setting_name] = $setting_value; 269 // Audit log of changes 270 Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->getUserName() . '"', $this); 271 } 272 273 return $this; 274 } 275 276 /** 277 * Can a user accept changes for this tree? 278 * 279 * @param User $user 280 * 281 * @return bool 282 */ 283 public function canAcceptChanges(User $user) 284 { 285 return Auth::isModerator($this, $user); 286 } 287 288 /** 289 * Fetch all the trees that we have permission to access. 290 * 291 * @return Tree[] 292 */ 293 public static function getAll() 294 { 295 if (self::$trees === null) { 296 self::$trees = []; 297 $rows = Database::prepare( 298 "SELECT g.gedcom_id AS tree_id, g.gedcom_name AS tree_name, gs1.setting_value AS tree_title" . 299 " FROM `##gedcom` g" . 300 " LEFT JOIN `##gedcom_setting` gs1 ON (g.gedcom_id=gs1.gedcom_id AND gs1.setting_name='title')" . 301 " LEFT JOIN `##gedcom_setting` gs2 ON (g.gedcom_id=gs2.gedcom_id AND gs2.setting_name='imported')" . 302 " LEFT JOIN `##gedcom_setting` gs3 ON (g.gedcom_id=gs3.gedcom_id AND gs3.setting_name='REQUIRE_AUTHENTICATION')" . 303 " LEFT JOIN `##user_gedcom_setting` ugs ON (g.gedcom_id=ugs.gedcom_id AND ugs.setting_name='canedit' AND ugs.user_id=?)" . 304 " WHERE " . 305 " g.gedcom_id>0 AND (" . // exclude the "template" tree 306 " EXISTS (SELECT 1 FROM `##user_setting` WHERE user_id=? AND setting_name='canadmin' AND setting_value=1)" . // Admin sees all 307 " ) OR (" . 308 " (gs2.setting_value = 1 OR ugs.setting_value = 'admin') AND (" . // Allow imported trees, with either: 309 " gs3.setting_value <> 1 OR" . // visitor access 310 " IFNULL(ugs.setting_value, 'none')<>'none'" . // explicit access 311 " )" . 312 " )" . 313 " ORDER BY g.sort_order, 3" 314 )->execute([ 315 Auth::id(), 316 Auth::id(), 317 ])->fetchAll(); 318 foreach ($rows as $row) { 319 self::$trees[$row->tree_name] = new self((int)$row->tree_id, $row->tree_name, $row->tree_title); 320 } 321 } 322 323 return self::$trees; 324 } 325 326 /** 327 * Find the tree with a specific ID. 328 * 329 * @param int $tree_id 330 * 331 * @throws \DomainException 332 * 333 * @return Tree 334 */ 335 public static function findById($tree_id) 336 { 337 foreach (self::getAll() as $tree) { 338 if ($tree->tree_id == $tree_id) { 339 return $tree; 340 } 341 } 342 throw new \DomainException; 343 } 344 345 /** 346 * Find the tree with a specific name. 347 * 348 * @param string $tree_name 349 * 350 * @return Tree|null 351 */ 352 public static function findByName($tree_name) 353 { 354 foreach (self::getAll() as $tree) { 355 if ($tree->name === $tree_name) { 356 return $tree; 357 } 358 } 359 360 return null; 361 } 362 363 /** 364 * Create arguments to select_edit_control() 365 * Note - these will be escaped later 366 * 367 * @return string[] 368 */ 369 public static function getIdList() 370 { 371 $list = []; 372 foreach (self::getAll() as $tree) { 373 $list[$tree->tree_id] = $tree->title; 374 } 375 376 return $list; 377 } 378 379 /** 380 * Create arguments to select_edit_control() 381 * Note - these will be escaped later 382 * 383 * @return string[] 384 */ 385 public static function getNameList() 386 { 387 $list = []; 388 foreach (self::getAll() as $tree) { 389 $list[$tree->name] = $tree->title; 390 } 391 392 return $list; 393 } 394 395 /** 396 * Create a new tree 397 * 398 * @param string $tree_name 399 * @param string $tree_title 400 * 401 * @return Tree 402 */ 403 public static function create($tree_name, $tree_title) 404 { 405 try { 406 // Create a new tree 407 Database::prepare( 408 "INSERT INTO `##gedcom` (gedcom_name) VALUES (?)" 409 )->execute([$tree_name]); 410 $tree_id = Database::prepare("SELECT LAST_INSERT_ID()")->fetchOne(); 411 } catch (PDOException $ex) { 412 DebugBar::addThrowable($ex); 413 414 // A tree with that name already exists? 415 return self::findByName($tree_name); 416 } 417 418 // Update the list of trees - to include this new one 419 self::$trees = null; 420 $tree = self::findById($tree_id); 421 422 $tree->setPreference('imported', '0'); 423 $tree->setPreference('title', $tree_title); 424 425 // Module privacy 426 Module::setDefaultAccess($tree_id); 427 428 // Set preferences from default tree 429 Database::prepare( 430 "INSERT INTO `##gedcom_setting` (gedcom_id, setting_name, setting_value)" . 431 " SELECT :tree_id, setting_name, setting_value" . 432 " FROM `##gedcom_setting` WHERE gedcom_id = -1" 433 )->execute([ 434 'tree_id' => $tree_id, 435 ]); 436 437 Database::prepare( 438 "INSERT INTO `##default_resn` (gedcom_id, tag_type, resn)" . 439 " SELECT :tree_id, tag_type, resn" . 440 " FROM `##default_resn` WHERE gedcom_id = -1" 441 )->execute([ 442 'tree_id' => $tree_id, 443 ]); 444 445 Database::prepare( 446 "INSERT INTO `##block` (gedcom_id, location, block_order, module_name)" . 447 " SELECT :tree_id, location, block_order, module_name" . 448 " FROM `##block` WHERE gedcom_id = -1" 449 )->execute([ 450 'tree_id' => $tree_id, 451 ]); 452 453 // Gedcom and privacy settings 454 $tree->setPreference('CONTACT_USER_ID', Auth::id()); 455 $tree->setPreference('WEBMASTER_USER_ID', Auth::id()); 456 $tree->setPreference('LANGUAGE', WT_LOCALE); // Default to the current admin’s language 457 switch (WT_LOCALE) { 458 case 'es': 459 $tree->setPreference('SURNAME_TRADITION', 'spanish'); 460 break; 461 case 'is': 462 $tree->setPreference('SURNAME_TRADITION', 'icelandic'); 463 break; 464 case 'lt': 465 $tree->setPreference('SURNAME_TRADITION', 'lithuanian'); 466 break; 467 case 'pl': 468 $tree->setPreference('SURNAME_TRADITION', 'polish'); 469 break; 470 case 'pt': 471 case 'pt-BR': 472 $tree->setPreference('SURNAME_TRADITION', 'portuguese'); 473 break; 474 default: 475 $tree->setPreference('SURNAME_TRADITION', 'paternal'); 476 break; 477 } 478 479 // Genealogy data 480 // It is simpler to create a temporary/unimported GEDCOM than to populate all the tables... 481 $john_doe = /* I18N: This should be a common/default/placeholder name of an individual. Put slashes around the surname. */ 482 I18N::translate('John /DOE/'); 483 $note = I18N::translate('Edit this individual and replace their details with your own.'); 484 Database::prepare("INSERT INTO `##gedcom_chunk` (gedcom_id, chunk_data) VALUES (?, ?)")->execute([ 485 $tree_id, 486 "0 HEAD\n1 CHAR UTF-8\n0 @I1@ INDI\n1 NAME {$john_doe}\n1 SEX M\n1 BIRT\n2 DATE 01 JAN 1850\n2 NOTE {$note}\n0 TRLR\n", 487 ]); 488 489 // Update our cache 490 self::$trees[$tree->tree_id] = $tree; 491 492 return $tree; 493 } 494 495 /** 496 * Are there any pending edits for this tree, than need reviewing by a moderator. 497 * 498 * @return bool 499 */ 500 public function hasPendingEdit() 501 { 502 return (bool)Database::prepare( 503 "SELECT 1 FROM `##change` WHERE status = 'pending' AND gedcom_id = :tree_id" 504 )->execute([ 505 'tree_id' => $this->tree_id, 506 ])->fetchOne(); 507 } 508 509 /** 510 * Delete all the genealogy data from a tree - in preparation for importing 511 * new data. Optionally retain the media data, for when the user has been 512 * editing their data offline using an application which deletes (or does not 513 * support) media data. 514 * 515 * @param bool $keep_media 516 */ 517 public function deleteGenealogyData($keep_media) 518 { 519 Database::prepare("DELETE FROM `##gedcom_chunk` WHERE gedcom_id = ?")->execute([$this->tree_id]); 520 Database::prepare("DELETE FROM `##individuals` WHERE i_file = ?")->execute([$this->tree_id]); 521 Database::prepare("DELETE FROM `##families` WHERE f_file = ?")->execute([$this->tree_id]); 522 Database::prepare("DELETE FROM `##sources` WHERE s_file = ?")->execute([$this->tree_id]); 523 Database::prepare("DELETE FROM `##other` WHERE o_file = ?")->execute([$this->tree_id]); 524 Database::prepare("DELETE FROM `##places` WHERE p_file = ?")->execute([$this->tree_id]); 525 Database::prepare("DELETE FROM `##placelinks` WHERE pl_file = ?")->execute([$this->tree_id]); 526 Database::prepare("DELETE FROM `##name` WHERE n_file = ?")->execute([$this->tree_id]); 527 Database::prepare("DELETE FROM `##dates` WHERE d_file = ?")->execute([$this->tree_id]); 528 Database::prepare("DELETE FROM `##change` WHERE gedcom_id = ?")->execute([$this->tree_id]); 529 530 if ($keep_media) { 531 Database::prepare("DELETE FROM `##link` WHERE l_file =? AND l_type<>'OBJE'")->execute([$this->tree_id]); 532 } else { 533 Database::prepare("DELETE FROM `##link` WHERE l_file =?")->execute([$this->tree_id]); 534 Database::prepare("DELETE FROM `##media` WHERE m_file =?")->execute([$this->tree_id]); 535 Database::prepare("DELETE FROM `##media_file` WHERE m_file =?")->execute([$this->tree_id]); 536 } 537 } 538 539 /** 540 * Delete everything relating to a tree 541 */ 542 public function delete() 543 { 544 // If this is the default tree, then unset it 545 if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) { 546 Site::setPreference('DEFAULT_GEDCOM', ''); 547 } 548 549 $this->deleteGenealogyData(false); 550 551 Database::prepare("DELETE `##block_setting` FROM `##block_setting` JOIN `##block` USING (block_id) WHERE gedcom_id=?")->execute([$this->tree_id]); 552 Database::prepare("DELETE FROM `##block` WHERE gedcom_id = ?")->execute([$this->tree_id]); 553 Database::prepare("DELETE FROM `##user_gedcom_setting` WHERE gedcom_id = ?")->execute([$this->tree_id]); 554 Database::prepare("DELETE FROM `##gedcom_setting` WHERE gedcom_id = ?")->execute([$this->tree_id]); 555 Database::prepare("DELETE FROM `##module_privacy` WHERE gedcom_id = ?")->execute([$this->tree_id]); 556 Database::prepare("DELETE FROM `##hit_counter` WHERE gedcom_id = ?")->execute([$this->tree_id]); 557 Database::prepare("DELETE FROM `##default_resn` WHERE gedcom_id = ?")->execute([$this->tree_id]); 558 Database::prepare("DELETE FROM `##gedcom_chunk` WHERE gedcom_id = ?")->execute([$this->tree_id]); 559 Database::prepare("DELETE FROM `##log` WHERE gedcom_id = ?")->execute([$this->tree_id]); 560 Database::prepare("DELETE FROM `##gedcom` WHERE gedcom_id = ?")->execute([$this->tree_id]); 561 562 // After updating the database, we need to fetch a new (sorted) copy 563 self::$trees = null; 564 } 565 566 /** 567 * Export the tree to a GEDCOM file 568 * 569 * @param resource $stream 570 */ 571 public function exportGedcom($stream) 572 { 573 $stmt = Database::prepare( 574 "SELECT i_gedcom AS gedcom, i_id AS xref, 1 AS n FROM `##individuals` WHERE i_file = :tree_id_1" . 575 " UNION ALL " . 576 "SELECT f_gedcom AS gedcom, f_id AS xref, 2 AS n FROM `##families` WHERE f_file = :tree_id_2" . 577 " UNION ALL " . 578 "SELECT s_gedcom AS gedcom, s_id AS xref, 3 AS n FROM `##sources` WHERE s_file = :tree_id_3" . 579 " UNION ALL " . 580 "SELECT o_gedcom AS gedcom, o_id AS xref, 4 AS n FROM `##other` WHERE o_file = :tree_id_4 AND o_type NOT IN ('HEAD', 'TRLR')" . 581 " UNION ALL " . 582 "SELECT m_gedcom AS gedcom, m_id AS xref, 5 AS n FROM `##media` WHERE m_file = :tree_id_5" . 583 " ORDER BY n, LENGTH(xref), xref" 584 )->execute([ 585 'tree_id_1' => $this->tree_id, 586 'tree_id_2' => $this->tree_id, 587 'tree_id_3' => $this->tree_id, 588 'tree_id_4' => $this->tree_id, 589 'tree_id_5' => $this->tree_id, 590 ]); 591 592 $buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this)); 593 while (($row = $stmt->fetch()) !== false) { 594 $buffer .= FunctionsExport::reformatRecord($row->gedcom); 595 if (strlen($buffer) > 65535) { 596 fwrite($stream, $buffer); 597 $buffer = ''; 598 } 599 } 600 fwrite($stream, $buffer . '0 TRLR' . Gedcom::EOL); 601 $stmt->closeCursor(); 602 } 603 604 /** 605 * Import data from a gedcom file into this tree. 606 * 607 * @param string $path The full path to the (possibly temporary) file. 608 * @param string $filename The preferred filename, for export/download. 609 * 610 * @throws \Exception 611 */ 612 public function importGedcomFile($path, $filename) 613 { 614 // Read the file in blocks of roughly 64K. Ensure that each block 615 // contains complete gedcom records. This will ensure we don’t split 616 // multi-byte characters, as well as simplifying the code to import 617 // each block. 618 619 $file_data = ''; 620 $fp = fopen($path, 'rb'); 621 622 // Don’t allow the user to cancel the request. We do not want to be left with an incomplete transaction. 623 ignore_user_abort(true); 624 625 $this->deleteGenealogyData($this->getPreference('keep_media')); 626 $this->setPreference('gedcom_filename', $filename); 627 $this->setPreference('imported', '0'); 628 629 while (!feof($fp)) { 630 $file_data .= fread($fp, 65536); 631 // There is no strrpos() function that searches for substrings :-( 632 for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) { 633 if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) { 634 // We’ve found the last record boundary in this chunk of data 635 break; 636 } 637 } 638 if ($pos) { 639 Database::prepare( 640 "INSERT INTO `##gedcom_chunk` (gedcom_id, chunk_data) VALUES (?, ?)" 641 )->execute([ 642 $this->tree_id, 643 substr($file_data, 0, $pos), 644 ]); 645 $file_data = substr($file_data, $pos); 646 } 647 } 648 Database::prepare( 649 "INSERT INTO `##gedcom_chunk` (gedcom_id, chunk_data) VALUES (?, ?)" 650 )->execute([ 651 $this->tree_id, 652 $file_data, 653 ]); 654 655 fclose($fp); 656 } 657 658 /** 659 * Generate a new XREF, unique across all family trees 660 * 661 * @return string 662 */ 663 public function getNewXref() 664 { 665 $prefix = 'X'; 666 667 $increment = 1.0; 668 do { 669 // Use LAST_INSERT_ID(expr) to provide a transaction-safe sequence. See 670 // http://dev.mysql.com/doc/refman/5.6/en/information-functions.html#function_last-insert-id 671 $statement = Database::prepare( 672 "UPDATE `##site_setting` SET setting_value = LAST_INSERT_ID(setting_value + :increment) WHERE setting_name = 'next_xref'" 673 ); 674 $statement->execute([ 675 'increment' => (int)$increment, 676 ]); 677 678 if ($statement->rowCount() === 0) { 679 // First time we've used this record type. 680 Site::setPreference('next_xref', '1'); 681 $num = 1; 682 } else { 683 $num = Database::prepare("SELECT LAST_INSERT_ID()")->fetchOne(); 684 } 685 686 $xref = $prefix . $num; 687 688 // Records may already exist with this sequence number. 689 $already_used = Database::prepare( 690 "SELECT" . 691 " EXISTS (SELECT 1 FROM `##individuals` WHERE i_id = :i_id) OR" . 692 " EXISTS (SELECT 1 FROM `##families` WHERE f_id = :f_id) OR" . 693 " EXISTS (SELECT 1 FROM `##sources` WHERE s_id = :s_id) OR" . 694 " EXISTS (SELECT 1 FROM `##media` WHERE m_id = :m_id) OR" . 695 " EXISTS (SELECT 1 FROM `##other` WHERE o_id = :o_id) OR" . 696 " EXISTS (SELECT 1 FROM `##change` WHERE xref = :xref)" 697 )->execute([ 698 'i_id' => $xref, 699 'f_id' => $xref, 700 's_id' => $xref, 701 'm_id' => $xref, 702 'o_id' => $xref, 703 'xref' => $xref, 704 ])->fetchOne(); 705 706 // This exponential increment allows us to scan over large blocks of 707 // existing data in a reasonable time. 708 $increment *= 1.01; 709 } while ($already_used !== '0'); 710 711 return $xref; 712 } 713 714 /** 715 * Create a new record from GEDCOM data. 716 * 717 * @param string $gedcom 718 * 719 * @throws \Exception 720 * 721 * @return GedcomRecord|Individual|Family|Note|Source|Repository|Media 722 */ 723 public function createRecord($gedcom) 724 { 725 if (preg_match('/^0 @(' . WT_REGEX_XREF . ')@ (' . WT_REGEX_TAG . ')/', $gedcom, $match)) { 726 $xref = $match[1]; 727 $type = $match[2]; 728 } else { 729 throw new \Exception('Invalid argument to GedcomRecord::createRecord(' . $gedcom . ')'); 730 } 731 if (strpos("\r", $gedcom) !== false) { 732 // MSDOS line endings will break things in horrible ways 733 throw new \Exception('Evil line endings found in GedcomRecord::createRecord(' . $gedcom . ')'); 734 } 735 736 // webtrees creates XREFs containing digits. Anything else (e.g. “new”) is just a placeholder. 737 if (!preg_match('/\d/', $xref)) { 738 $xref = $this->getNewXref(); 739 $gedcom = preg_replace('/^0 @(' . WT_REGEX_XREF . ')@/', '0 @' . $xref . '@', $gedcom); 740 } 741 742 // Create a change record, if not already present 743 if (!preg_match('/\n1 CHAN/', $gedcom)) { 744 $gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->getUserName(); 745 } 746 747 // Create a pending change 748 Database::prepare( 749 "INSERT INTO `##change` (gedcom_id, xref, old_gedcom, new_gedcom, user_id) VALUES (?, ?, '', ?, ?)" 750 )->execute([ 751 $this->tree_id, 752 $xref, 753 $gedcom, 754 Auth::id(), 755 ]); 756 757 Log::addEditLog('Create: ' . $type . ' ' . $xref, $this); 758 759 // Accept this pending change 760 if (Auth::user()->getPreference('auto_accept')) { 761 FunctionsImport::acceptAllChanges($xref, $this); 762 } 763 // Return the newly created record. Note that since GedcomRecord 764 // has a cache of pending changes, we cannot use it to create a 765 // record with a newly created pending change. 766 return GedcomRecord::getInstance($xref, $this, $gedcom); 767 } 768 769 /** 770 * What is the most significant individual in this tree. 771 * 772 * @param User $user 773 * 774 * @return Individual 775 */ 776 public function significantIndividual(User $user): Individual 777 { 778 static $individual; // Only query the DB once. 779 780 if (!$individual && $this->getUserPreference($user, 'rootid')) { 781 $individual = Individual::getInstance($this->getUserPreference($user, 'rootid'), $this); 782 } 783 if (!$individual && $this->getUserPreference($user, 'gedcomid')) { 784 $individual = Individual::getInstance($this->getUserPreference($user, 'gedcomid'), $this); 785 } 786 if (!$individual) { 787 $individual = Individual::getInstance($this->getPreference('PEDIGREE_ROOT_ID'), $this); 788 } 789 if (!$individual) { 790 $individual = Individual::getInstance( 791 Database::prepare( 792 "SELECT MIN(i_id) FROM `##individuals` WHERE i_file = :tree_id" 793 )->execute([ 794 'tree_id' => $this->getTreeId(), 795 ])->fetchOne(), 796 $this 797 ); 798 } 799 if (!$individual) { 800 // always return a record 801 $individual = new Individual('I', '0 @I@ INDI', null, $this); 802 } 803 804 return $individual; 805 } 806 807 /** 808 * Get significant information from this page, to allow other pages such as 809 * charts and reports to initialise with the same records 810 * 811 * @return Individual 812 */ 813 public function getSignificantIndividual() 814 { 815 static $individual; // Only query the DB once. 816 817 if (!$individual && $this->getUserPreference(Auth::user(), 'rootid')) { 818 $individual = Individual::getInstance($this->getUserPreference(Auth::user(), 'rootid'), $this); 819 } 820 if (!$individual && $this->getUserPreference(Auth::user(), 'gedcomid')) { 821 $individual = Individual::getInstance($this->getUserPreference(Auth::user(), 'gedcomid'), $this); 822 } 823 if (!$individual) { 824 $individual = Individual::getInstance($this->getPreference('PEDIGREE_ROOT_ID'), $this); 825 } 826 if (!$individual) { 827 $individual = Individual::getInstance( 828 Database::prepare( 829 "SELECT MIN(i_id) FROM `##individuals` WHERE i_file=?" 830 )->execute([$this->getTreeId()])->fetchOne(), 831 $this 832 ); 833 } 834 if (!$individual) { 835 // always return a record 836 $individual = new Individual('I', '0 @I@ INDI', null, $this); 837 } 838 839 return $individual; 840 } 841} 842