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