xref: /webtrees/app/Fact.php (revision 73df4c545c87012519818766e1a8eb02272a59b5)
1<?php
2/**
3 * webtrees: online genealogy
4 * Copyright (C) 2016 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|null
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 null;
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|null
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 null;
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(Filter::escapeHtml($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 = array();
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 = array();
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 = array();
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 = array();
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">' . Filter::escapeHtml($value) . '</span>';
387			}
388			// Fact date
389			$date = $this->getDate();
390			if ($date->isOK()) {
391				if ($this->getTag() === '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				array(
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') !== null && $factsort[$atag] > $factsort['BURI'] && $factsort[$atag] < $factsort['CHAN']) {
551			$atag = 'BURI';
552		}
553
554		if ($b->getAttribute('DATE') !== null && $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') !== null && $b->getAttribute('DATE') === null) {
563				return -1;
564			}
565
566			if ($b->getAttribute('DATE') !== null && $a->getAttribute('DATE') === null) {
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