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\FunctionsPrint; 19use InvalidArgumentException; 20 21/** 22 * A GEDCOM fact or event object. 23 */ 24class Fact 25{ 26 /** @var string Unique identifier for this fact (currently implemented as a hash of the raw data). */ 27 private $fact_id; 28 29 /** @var GedcomRecord The GEDCOM record from which this fact is taken */ 30 private $parent; 31 32 /** @var string The raw GEDCOM data for this fact */ 33 private $gedcom; 34 35 /** @var string The GEDCOM tag for this record */ 36 private $tag; 37 38 /** @var bool Is this a recently deleted fact, pending approval? */ 39 private $pending_deletion = false; 40 41 /** @var bool Is this a recently added fact, pending approval? */ 42 private $pending_addition = false; 43 44 /** @var Date The date of this fact, from the “2 DATE …” attribute */ 45 private $date; 46 47 /** @var Place The place of this fact, from the “2 PLAC …” attribute */ 48 private $place; 49 50 /** @var int Temporary(!) variable Used by Functions::sortFacts() */ 51 public $sortOrder; 52 53 /** 54 * Create an event object from a gedcom fragment. 55 * We need the parent object (to check privacy) and a (pseudo) fact ID to 56 * identify the fact within the record. 57 * 58 * @param string $gedcom 59 * @param GedcomRecord $parent 60 * @param string $fact_id 61 * 62 * @throws InvalidArgumentException 63 */ 64 public function __construct($gedcom, GedcomRecord $parent, $fact_id) 65 { 66 if (preg_match('/^1 (' . WT_REGEX_TAG . ')/', $gedcom, $match)) { 67 $this->gedcom = $gedcom; 68 $this->parent = $parent; 69 $this->fact_id = $fact_id; 70 $this->tag = $match[1]; 71 } else { 72 throw new InvalidArgumentException('Invalid GEDCOM data passed to Fact::_construct(' . $gedcom . ')'); 73 } 74 } 75 76 /** 77 * Get the value of level 1 data in the fact 78 * Allow for multi-line values 79 * 80 * @return string 81 */ 82 public function getValue() 83 { 84 if (preg_match('/^1 (?:' . $this->tag . ') ?(.*(?:(?:\n2 CONT ?.*)*))/', $this->gedcom, $match)) { 85 return preg_replace("/\n2 CONT ?/", "\n", $match[1]); 86 } else { 87 return ''; 88 } 89 } 90 91 /** 92 * Get the record to which this fact links 93 * 94 * @return Individual|Family|Source|Repository|Media|Note|null 95 */ 96 public function getTarget() 97 { 98 $xref = trim($this->getValue(), '@'); 99 switch ($this->tag) { 100 case 'FAMC': 101 case 'FAMS': 102 return Family::getInstance($xref, $this->getParent()->getTree()); 103 case 'HUSB': 104 case 'WIFE': 105 case 'CHIL': 106 return Individual::getInstance($xref, $this->getParent()->getTree()); 107 case 'SOUR': 108 return Source::getInstance($xref, $this->getParent()->getTree()); 109 case 'OBJE': 110 return Media::getInstance($xref, $this->getParent()->getTree()); 111 case 'REPO': 112 return Repository::getInstance($xref, $this->getParent()->getTree()); 113 case 'NOTE': 114 return Note::getInstance($xref, $this->getParent()->getTree()); 115 default: 116 return GedcomRecord::getInstance($xref, $this->getParent()->getTree()); 117 } 118 } 119 120 /** 121 * Get the value of level 2 data in the fact 122 * 123 * @param string $tag 124 * 125 * @return string 126 */ 127 public function getAttribute($tag) 128 { 129 if (preg_match('/\n2 (?:' . $tag . ') ?(.*(?:(?:\n3 CONT ?.*)*)*)/', $this->gedcom, $match)) { 130 return preg_replace("/\n3 CONT ?/", "\n", $match[1]); 131 } else { 132 return ''; 133 } 134 } 135 136 /** 137 * Do the privacy rules allow us to display this fact to the current user 138 * 139 * @param int|null $access_level 140 * 141 * @return bool 142 */ 143 public function canShow($access_level = null) 144 { 145 if ($access_level === null) { 146 $access_level = Auth::accessLevel($this->getParent()->getTree()); 147 } 148 149 // Does this record have an explicit RESN? 150 if (strpos($this->gedcom, "\n2 RESN confidential")) { 151 return Auth::PRIV_NONE >= $access_level; 152 } 153 if (strpos($this->gedcom, "\n2 RESN privacy")) { 154 return Auth::PRIV_USER >= $access_level; 155 } 156 if (strpos($this->gedcom, "\n2 RESN none")) { 157 return true; 158 } 159 160 // Does this record have a default RESN? 161 $xref = $this->parent->getXref(); 162 $fact_privacy = $this->parent->getTree()->getFactPrivacy(); 163 $individual_fact_privacy = $this->parent->getTree()->getIndividualFactPrivacy(); 164 if (isset($individual_fact_privacy[$xref][$this->tag])) { 165 return $individual_fact_privacy[$xref][$this->tag] >= $access_level; 166 } 167 if (isset($fact_privacy[$this->tag])) { 168 return $fact_privacy[$this->tag] >= $access_level; 169 } 170 171 // No restrictions - it must be public 172 return true; 173 } 174 175 /** 176 * Check whether this fact is protected against edit 177 * 178 * @return bool 179 */ 180 public function canEdit() 181 { 182 // Managers can edit anything 183 // Members cannot edit RESN, CHAN and locked records 184 return 185 $this->parent->canEdit() && !$this->isPendingDeletion() && ( 186 Auth::isManager($this->parent->getTree()) || 187 Auth::isEditor($this->parent->getTree()) && strpos($this->gedcom, "\n2 RESN locked") === false && $this->getTag() != 'RESN' && $this->getTag() != 'CHAN' 188 ); 189 } 190 191 /** 192 * The place where the event occured. 193 * 194 * @return Place 195 */ 196 public function getPlace() 197 { 198 if ($this->place === null) { 199 $this->place = new Place($this->getAttribute('PLAC'), $this->getParent()->getTree()); 200 } 201 202 return $this->place; 203 } 204 205 /** 206 * Get the date for this fact. 207 * We can call this function many times, especially when sorting, 208 * so keep a copy of the date. 209 * 210 * @return Date 211 */ 212 public function getDate() 213 { 214 if ($this->date === null) { 215 $this->date = new Date($this->getAttribute('DATE')); 216 } 217 218 return $this->date; 219 } 220 221 /** 222 * The raw GEDCOM data for this fact 223 * 224 * @return string 225 */ 226 public function getGedcom() 227 { 228 return $this->gedcom; 229 } 230 231 /** 232 * Get a (pseudo) primary key for this fact. 233 * 234 * @return string 235 */ 236 public function getFactId() 237 { 238 return $this->fact_id; 239 } 240 241 /** 242 * What is the tag (type) of this fact, such as BIRT, MARR or DEAT. 243 * 244 * @return string 245 */ 246 public function getTag() 247 { 248 return $this->tag; 249 } 250 251 /** 252 * Used to convert a real fact (e.g. BIRT) into a close-relative’s fact (e.g. _BIRT_CHIL) 253 * 254 * @param string $tag 255 */ 256 public function setTag($tag) 257 { 258 $this->tag = $tag; 259 } 260 261 /** 262 * The Person/Family record where this Fact came from 263 * 264 * @return Individual|Family|Source|Repository|Media|Note|GedcomRecord 265 */ 266 public function getParent() 267 { 268 return $this->parent; 269 } 270 271 /** 272 * Get the name of this fact type, for use as a label. 273 * 274 * @return string 275 */ 276 public function getLabel() 277 { 278 // Custom FACT/EVEN - with a TYPE 279 if (($this->tag === 'FACT' || $this->tag === 'EVEN') && $this->getAttribute('TYPE') !== '') { 280 return I18N::translate(e($this->getAttribute('TYPE'))); 281 } 282 283 return GedcomTag::getLabel($this->tag, $this->parent); 284 } 285 286 /** 287 * This is a newly deleted fact, pending approval. 288 */ 289 public function setPendingDeletion() 290 { 291 $this->pending_deletion = true; 292 $this->pending_addition = false; 293 } 294 295 /** 296 * Is this a newly deleted fact, pending approval. 297 * 298 * @return bool 299 */ 300 public function isPendingDeletion() 301 { 302 return $this->pending_deletion; 303 } 304 305 /** 306 * This is a newly added fact, pending approval. 307 */ 308 public function setPendingAddition() 309 { 310 $this->pending_addition = true; 311 $this->pending_deletion = false; 312 } 313 314 /** 315 * Is this a newly added fact, pending approval. 316 * 317 * @return bool 318 */ 319 public function isPendingAddition() 320 { 321 return $this->pending_addition; 322 } 323 324 /** 325 * Source citations linked to this fact 326 * 327 * @return string[] 328 */ 329 public function getCitations() 330 { 331 preg_match_all('/\n(2 SOUR @(' . WT_REGEX_XREF . ')@(?:\n[3-9] .*)*)/', $this->getGedcom(), $matches, PREG_SET_ORDER); 332 $citations = []; 333 foreach ($matches as $match) { 334 $source = Source::getInstance($match[2], $this->getParent()->getTree()); 335 if ($source->canShow()) { 336 $citations[] = $match[1]; 337 } 338 } 339 340 return $citations; 341 } 342 343 /** 344 * Notes (inline and objects) linked to this fact 345 * 346 * @return string[]|Note[] 347 */ 348 public function getNotes() 349 { 350 $notes = []; 351 preg_match_all('/\n2 NOTE ?(.*(?:\n3.*)*)/', $this->getGedcom(), $matches); 352 foreach ($matches[1] as $match) { 353 $note = preg_replace("/\n3 CONT ?/", "\n", $match); 354 if (preg_match('/@(' . WT_REGEX_XREF . ')@/', $note, $nmatch)) { 355 $note = Note::getInstance($nmatch[1], $this->getParent()->getTree()); 356 if ($note && $note->canShow()) { 357 // A note object 358 $notes[] = $note; 359 } 360 } else { 361 // An inline note 362 $notes[] = $note; 363 } 364 } 365 366 return $notes; 367 } 368 369 /** 370 * Media objects linked to this fact 371 * 372 * @return Media[] 373 */ 374 public function getMedia() 375 { 376 $media = []; 377 preg_match_all('/\n2 OBJE @(' . WT_REGEX_XREF . ')@/', $this->getGedcom(), $matches); 378 foreach ($matches[1] as $match) { 379 $obje = Media::getInstance($match, $this->getParent()->getTree()); 380 if ($obje->canShow()) { 381 $media[] = $obje; 382 } 383 } 384 385 return $media; 386 } 387 388 /** 389 * A one-line summary of the fact - for charts, etc. 390 * 391 * @return string 392 */ 393 public function summary() 394 { 395 $attributes = []; 396 $target = $this->getTarget(); 397 if ($target) { 398 $attributes[] = $target->getFullName(); 399 } else { 400 // Fact value 401 $value = $this->getValue(); 402 if ($value !== '' && $value !== 'Y') { 403 $attributes[] = '<span dir="auto">' . e($value) . '</span>'; 404 } 405 // Fact date 406 $date = $this->getDate(); 407 if ($date->isOK()) { 408 if (in_array($this->getTag(), explode('|', WT_EVENTS_BIRT)) && $this->getParent() instanceof Individual && $this->getParent()->getTree()->getPreference('SHOW_PARENTS_AGE')) { 409 $attributes[] = $date->display() . FunctionsPrint::formatParentsAges($this->getParent(), $date); 410 } else { 411 $attributes[] = $date->display(); 412 } 413 } 414 // Fact place 415 if (!$this->getPlace()->isEmpty()) { 416 $attributes[] = $this->getPlace()->getShortName(); 417 } 418 } 419 420 $class = 'fact_' . $this->getTag(); 421 if ($this->isPendingAddition()) { 422 $class .= ' new'; 423 } elseif ($this->isPendingDeletion()) { 424 $class .= ' old'; 425 } 426 427 return 428 '<div class="' . $class . '">' . 429 /* I18N: a label/value pair, such as “Occupation: Farmer”. Some languages may need to change the punctuation. */ 430 I18N::translate('<span class="label">%1$s:</span> <span class="field" dir="auto">%2$s</span>', $this->getLabel(), implode(' — ', $attributes)) . 431 '</div>'; 432 } 433 434 /** 435 * Static Helper functions to sort events 436 * 437 * @param Fact $a Fact one 438 * @param Fact $b Fact two 439 * 440 * @return int 441 */ 442 public static function compareDate(Fact $a, Fact $b) 443 { 444 if ($a->getDate()->isOK() && $b->getDate()->isOK()) { 445 // If both events have dates, compare by date 446 $ret = Date::compare($a->getDate(), $b->getDate()); 447 448 if ($ret == 0) { 449 // If dates are the same, compare by fact type 450 $ret = self::compareType($a, $b); 451 452 // If the fact type is also the same, retain the initial order 453 if ($ret == 0) { 454 $ret = $a->sortOrder - $b->sortOrder; 455 } 456 } 457 458 return $ret; 459 } else { 460 // One or both events have no date - retain the initial order 461 return $a->sortOrder - $b->sortOrder; 462 } 463 } 464 465 /** 466 * Static method to compare two events by their type. 467 * 468 * @param Fact $a Fact one 469 * @param Fact $b Fact two 470 * 471 * @return int 472 */ 473 public static function compareType(Fact $a, Fact $b) 474 { 475 global $factsort; 476 477 if (empty($factsort)) { 478 $factsort = array_flip( 479 [ 480 'BIRT', 481 '_HNM', 482 'ALIA', 483 '_AKA', 484 '_AKAN', 485 'ADOP', 486 '_ADPF', 487 '_ADPF', 488 '_BRTM', 489 'CHR', 490 'BAPM', 491 'FCOM', 492 'CONF', 493 'BARM', 494 'BASM', 495 'EDUC', 496 'GRAD', 497 '_DEG', 498 'EMIG', 499 'IMMI', 500 'NATU', 501 '_MILI', 502 '_MILT', 503 'ENGA', 504 'MARB', 505 'MARC', 506 'MARL', 507 '_MARI', 508 '_MBON', 509 'MARR', 510 'MARR_CIVIL', 511 'MARR_RELIGIOUS', 512 'MARR_PARTNERS', 513 'MARR_UNKNOWN', 514 '_COML', 515 '_STAT', 516 '_SEPR', 517 'DIVF', 518 'MARS', 519 '_BIRT_CHIL', 520 'DIV', 521 'ANUL', 522 '_BIRT_', 523 '_MARR_', 524 '_DEAT_', 525 '_BURI_', 526 'CENS', 527 'OCCU', 528 'RESI', 529 'PROP', 530 'CHRA', 531 'RETI', 532 'FACT', 533 'EVEN', 534 '_NMR', 535 '_NMAR', 536 'NMR', 537 'NCHI', 538 'WILL', 539 '_HOL', 540 '_????_', 541 'DEAT', 542 '_FNRL', 543 'CREM', 544 'BURI', 545 '_INTE', 546 '_YART', 547 '_NLIV', 548 'PROB', 549 'TITL', 550 'COMM', 551 'NATI', 552 'CITN', 553 'CAST', 554 'RELI', 555 'SSN', 556 'IDNO', 557 'TEMP', 558 'SLGC', 559 'BAPL', 560 'CONL', 561 'ENDL', 562 'SLGS', 563 'ADDR', 564 'PHON', 565 'EMAIL', 566 '_EMAIL', 567 'EMAL', 568 'FAX', 569 'WWW', 570 'URL', 571 '_URL', 572 'AFN', 573 'REFN', 574 '_PRMN', 575 'REF', 576 'RIN', 577 '_UID', 578 'OBJE', 579 'NOTE', 580 'SOUR', 581 'CHAN', 582 '_TODO', 583 ] 584 ); 585 } 586 587 // Facts from same families stay grouped together 588 // Keep MARR and DIV from the same families from mixing with events from other FAMs 589 // Use the original order in which the facts were added 590 if ($a->parent instanceof Family && $b->parent instanceof Family && $a->parent !== $b->parent) { 591 return $a->sortOrder - $b->sortOrder; 592 } 593 594 $atag = $a->getTag(); 595 $btag = $b->getTag(); 596 597 // Events not in the above list get mapped onto one that is. 598 if (!array_key_exists($atag, $factsort)) { 599 if (preg_match('/^(_(BIRT|MARR|DEAT|BURI)_)/', $atag, $match)) { 600 $atag = $match[1]; 601 } else { 602 $atag = '_????_'; 603 } 604 } 605 606 if (!array_key_exists($btag, $factsort)) { 607 if (preg_match('/^(_(BIRT|MARR|DEAT|BURI)_)/', $btag, $match)) { 608 $btag = $match[1]; 609 } else { 610 $btag = '_????_'; 611 } 612 } 613 614 // - Don't let dated after DEAT/BURI facts sort non-dated facts before DEAT/BURI 615 // - Treat dated after BURI facts as BURI instead 616 if ($a->getAttribute('DATE') !== '' && $factsort[$atag] > $factsort['BURI'] && $factsort[$atag] < $factsort['CHAN']) { 617 $atag = 'BURI'; 618 } 619 620 if ($b->getAttribute('DATE') !== '' && $factsort[$btag] > $factsort['BURI'] && $factsort[$btag] < $factsort['CHAN']) { 621 $btag = 'BURI'; 622 } 623 624 $ret = $factsort[$atag] - $factsort[$btag]; 625 626 // If facts are the same then put dated facts before non-dated facts 627 if ($ret == 0) { 628 if ($a->getAttribute('DATE') !== '' && $b->getAttribute('DATE') === '') { 629 return -1; 630 } 631 632 if ($b->getAttribute('DATE') !== '' && $a->getAttribute('DATE') === '') { 633 return 1; 634 } 635 636 // If no sorting preference, then keep original ordering 637 $ret = $a->sortOrder - $b->sortOrder; 638 } 639 640 return $ret; 641 } 642 643 /** 644 * Allow native PHP functions such as array_unique() to work with objects 645 * 646 * @return string 647 */ 648 public function __toString() 649 { 650 return $this->fact_id . '@' . $this->parent->getXref(); 651 } 652} 653