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