xref: /webtrees/app/Fact.php (revision 2836aa05d87d478edc3ef1f1983e9d37862ded10)
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