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