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