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