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