Completed
Push — develop ( 9087a8...c9b4ef )
by Greg
16:31 queued 05:44
created

GedcomRecord   F

Complexity

Total Complexity 199

Size/Duplication

Total Lines 1313
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1313
rs 0.6314
c 0
b 0
f 0
wmc 199

53 Methods

Rating   Name   Duplication   Size   Complexity  
A getTree() 0 2 1
F getInstance() 0 92 19
C fetchGedcomRecord() 0 34 7
A getXref() 0 2 1
A __construct() 0 7 1
A getGedcom() 0 5 2
C parseFacts() 0 29 8
A isPendingDeletion() 0 2 1
A extractNames() 0 2 1
B getSecondaryName() 0 18 7
A getRawUrl() 0 2 1
A canEdit() 0 2 3
A isPendingAddition() 0 2 1
A url() 0 4 1
A setPrimaryName() 0 3 1
A getFallBackName() 0 2 1
A getFullName() 0 7 2
A addName() 0 8 1
D canShow() 0 33 9
A privatizeGedcom() 0 20 4
A getSortName() 0 5 1
A getAllNames() 0 16 4
B getPrimaryName() 0 22 6
A canShowByType() 0 9 2
A getAddName() 0 7 3
B extractNamesFromFacts() 0 15 9
A compare() 0 12 4
A canShowName() 0 6 2
D canShowRecord() 0 35 9
A getHtmlUrl() 0 2 1
A __toString() 0 2 1
A createPrivateGedcomRecord() 0 2 1
A getAllEventDates() 0 9 3
B formatFirstMajorFact() 0 19 8
A linkedMedia() 0 21 3
A getAllEventPlaces() 0 11 4
A linkedFamilies() 0 22 3
A formatListDetails() 0 2 1
B getFacts() 0 18 9
B linkedNotes() 0 24 3
A createFact() 0 2 1
B linkedIndividuals() 0 24 3
A formatList() 0 7 1
B removeLinks() 0 14 5
A deleteRecord() 0 23 3
A linkedSources() 0 23 3
B linkedRepositories() 0 24 3
B updateRecord() 0 35 3
C updateFact() 0 64 15
A deleteFact() 0 2 1
B lastChangeTimestamp() 0 24 6
A lastChangeUser() 0 11 3
A getFirstFact() 0 8 3

How to fix   Complexity   

Complex Class

Complex classes like GedcomRecord often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GedcomRecord, and based on these observations, apply Extract Interface, too.

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
 */
16
namespace Fisharebest\Webtrees;
17
18
use Fisharebest\Webtrees\Functions\Functions;
19
use Fisharebest\Webtrees\Functions\FunctionsDate;
20
use Fisharebest\Webtrees\Functions\FunctionsImport;
21
use Fisharebest\Webtrees\Functions\FunctionsPrint;
22
23
/**
24
 * A GEDCOM object.
25
 */
26
class GedcomRecord {
27
	const RECORD_TYPE = 'UNKNOWN';
28
	const ROUTE_NAME  = 'record';
29
30
	/** @var string The record identifier */
31
	protected $xref;
32
33
	/** @var Tree  The family tree to which this record belongs */
34
	protected $tree;
35
36
	/** @var string  GEDCOM data (before any pending edits) */
37
	protected $gedcom;
38
39
	/** @var string|null  GEDCOM data (after any pending edits) */
40
	protected $pending;
41
42
	/** @var Fact[] facts extracted from $gedcom/$pending */
43
	protected $facts;
44
45
	/** @var bool Can we display details of this record to Auth::PRIV_PRIVATE */
46
	private $disp_public;
47
48
	/** @var bool Can we display details of this record to Auth::PRIV_USER */
49
	private $disp_user;
50
51
	/** @var bool Can we display details of this record to Auth::PRIV_NONE */
52
	private $disp_none;
53
54
	/** @var string[][] All the names of this individual */
55
	protected $_getAllNames;
56
57
	/** @var int Cached result */
58
	protected $_getPrimaryName;
59
60
	/** @var int Cached result */
61
	protected $_getSecondaryName;
62
63
	/** @var GedcomRecord[][] Allow getInstance() to return references to existing objects */
64
	protected static $gedcom_record_cache;
65
66
	/** @var \stdClass[][] Fetch all pending edits in one database query */
67
	private static $pending_record_cache;
68
69
	/**
70
	 * Create a GedcomRecord object from raw GEDCOM data.
71
	 *
72
	 * @param string      $xref
73
	 * @param string      $gedcom  an empty string for new/pending records
74
	 * @param string|null $pending null for a record with no pending edits,
75
	 *                             empty string for records with pending deletions
76
	 * @param Tree        $tree
77
	 */
78
	public function __construct($xref, $gedcom, $pending, $tree) {
79
		$this->xref    = $xref;
80
		$this->gedcom  = $gedcom;
81
		$this->pending = $pending;
82
		$this->tree    = $tree;
83
84
		$this->parseFacts();
85
	}
86
87
	/**
88
	 * Split the record into facts
89
	 */
90
	private function parseFacts() {
91
		// Split the record into facts
92
		if ($this->gedcom) {
93
			$gedcom_facts = preg_split('/\n(?=1)/s', $this->gedcom);
94
			array_shift($gedcom_facts);
0 ignored issues
show
Bug introduced by
It seems like $gedcom_facts can also be of type false; however, parameter $array of array_shift() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

94
			array_shift(/** @scrutinizer ignore-type */ $gedcom_facts);
Loading history...
95
		} else {
96
			$gedcom_facts = [];
97
		}
98
		if ($this->pending) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->pending of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
99
			$pending_facts = preg_split('/\n(?=1)/s', $this->pending);
100
			array_shift($pending_facts);
101
		} else {
102
			$pending_facts = [];
103
		}
104
105
		$this->facts = [];
106
107
		foreach ($gedcom_facts as $gedcom_fact) {
108
			$fact = new Fact($gedcom_fact, $this, md5($gedcom_fact));
109
			if ($this->pending !== null && !in_array($gedcom_fact, $pending_facts)) {
0 ignored issues
show
Bug introduced by
It seems like $pending_facts can also be of type false; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

109
			if ($this->pending !== null && !in_array($gedcom_fact, /** @scrutinizer ignore-type */ $pending_facts)) {
Loading history...
110
				$fact->setPendingDeletion();
111
			}
112
			$this->facts[] = $fact;
113
		}
114
		foreach ($pending_facts as $pending_fact) {
115
			if (!in_array($pending_fact, $gedcom_facts)) {
116
				$fact = new Fact($pending_fact, $this, md5($pending_fact));
117
				$fact->setPendingAddition();
118
				$this->facts[] = $fact;
119
			}
120
		}
121
	}
122
123
	/**
124
	 * Get an instance of a GedcomRecord object. For single records,
125
	 * we just receive the XREF. For bulk records (such as lists
126
	 * and search results) we can receive the GEDCOM data as well.
127
	 *
128
	 * @param string      $xref
129
	 * @param Tree        $tree
130
	 * @param string|null $gedcom
131
	 *
132
	 * @throws \Exception
133
	 *
134
	 * @return GedcomRecord|Individual|Family|Source|Repository|Media|Note|null
135
	 */
136
	public static function getInstance($xref, Tree $tree, $gedcom = null) {
137
		$tree_id = $tree->getTreeId();
138
139
		// Is this record already in the cache, and of the correct type?
140
		if (isset(self::$gedcom_record_cache[$xref][$tree_id])) {
141
			$record = self::$gedcom_record_cache[$xref][$tree_id];
142
143
			if ($record instanceof static) {
0 ignored issues
show
introduced by
The condition $record instanceof static can never be false since $record is always a sub-type of static.
Loading history...
144
				return $record;
145
			} else {
146
				null;
147
			}
148
		}
149
150
		// Do we need to fetch the record from the database?
151
		if ($gedcom === null) {
152
			$gedcom = static::fetchGedcomRecord($xref, $tree_id);
153
		}
154
155
		// If we can edit, then we also need to be able to see pending records.
156
		if (Auth::isEditor($tree)) {
157
			if (!isset(self::$pending_record_cache[$tree_id])) {
158
				// Fetch all pending records in one database query
159
				self::$pending_record_cache[$tree_id] = [];
160
				$rows                                 = Database::prepare(
161
					"SELECT xref, new_gedcom FROM `##change` WHERE status = 'pending' AND gedcom_id = :tree_id ORDER BY change_id"
162
				)->execute([
163
					'tree_id' => $tree_id,
164
				])->fetchAll();
165
				foreach ($rows as $row) {
166
					self::$pending_record_cache[$tree_id][$row->xref] = $row->new_gedcom;
167
				}
168
			}
169
170
			if (isset(self::$pending_record_cache[$tree_id][$xref])) {
171
				// A pending edit exists for this record
172
				$pending = self::$pending_record_cache[$tree_id][$xref];
173
			} else {
174
				$pending = null;
175
			}
176
		} else {
177
			// There are no pending changes for this record
178
			$pending = null;
179
		}
180
181
		// No such record exists
182
		if ($gedcom === null && $pending === null) {
183
			return null;
184
		}
185
186
		// Create the object
187
		if (preg_match('/^0 @(' . WT_REGEX_XREF . ')@ (' . WT_REGEX_TAG . ')/', $gedcom . $pending, $match)) {
0 ignored issues
show
Bug introduced by
Are you sure $pending of type stdClass|null can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

187
		if (preg_match('/^0 @(' . WT_REGEX_XREF . ')@ (' . WT_REGEX_TAG . ')/', $gedcom . /** @scrutinizer ignore-type */ $pending, $match)) {
Loading history...
188
			$xref = $match[1]; // Collation - we may have requested I123 and found i123
189
			$type = $match[2];
190
		} elseif (preg_match('/^0 (HEAD|TRLR)/', $gedcom . $pending, $match)) {
191
			$xref = $match[1];
192
			$type = $match[1];
193
		} elseif ($gedcom . $pending) {
194
			throw new \Exception('Unrecognized GEDCOM record: ' . $gedcom);
195
		} else {
196
			// A record with both pending creation and pending deletion
197
			$type = static::RECORD_TYPE;
198
		}
199
200
		switch ($type) {
201
			case 'INDI':
202
				$record = new Individual($xref, $gedcom, $pending, $tree);
0 ignored issues
show
Bug introduced by
It seems like $pending can also be of type stdClass; however, parameter $pending of Fisharebest\Webtrees\Individual::__construct() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

202
				$record = new Individual($xref, $gedcom, /** @scrutinizer ignore-type */ $pending, $tree);
Loading history...
203
				break;
204
			case 'FAM':
205
				$record = new Family($xref, $gedcom, $pending, $tree);
0 ignored issues
show
Bug introduced by
It seems like $pending can also be of type stdClass; however, parameter $pending of Fisharebest\Webtrees\Family::__construct() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

205
				$record = new Family($xref, $gedcom, /** @scrutinizer ignore-type */ $pending, $tree);
Loading history...
206
				break;
207
			case 'SOUR':
208
				$record = new Source($xref, $gedcom, $pending, $tree);
0 ignored issues
show
Bug introduced by
It seems like $pending can also be of type stdClass; however, parameter $pending of Fisharebest\Webtrees\Source::__construct() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

208
				$record = new Source($xref, $gedcom, /** @scrutinizer ignore-type */ $pending, $tree);
Loading history...
209
				break;
210
			case 'OBJE':
211
				$record = new Media($xref, $gedcom, $pending, $tree);
0 ignored issues
show
Bug introduced by
It seems like $pending can also be of type stdClass; however, parameter $pending of Fisharebest\Webtrees\Media::__construct() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

211
				$record = new Media($xref, $gedcom, /** @scrutinizer ignore-type */ $pending, $tree);
Loading history...
212
				break;
213
			case 'REPO':
214
				$record = new Repository($xref, $gedcom, $pending, $tree);
0 ignored issues
show
Bug introduced by
It seems like $pending can also be of type stdClass; however, parameter $pending of Fisharebest\Webtrees\Repository::__construct() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

214
				$record = new Repository($xref, $gedcom, /** @scrutinizer ignore-type */ $pending, $tree);
Loading history...
215
				break;
216
			case 'NOTE':
217
				$record = new Note($xref, $gedcom, $pending, $tree);
0 ignored issues
show
Bug introduced by
It seems like $pending can also be of type stdClass; however, parameter $pending of Fisharebest\Webtrees\Note::__construct() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

217
				$record = new Note($xref, $gedcom, /** @scrutinizer ignore-type */ $pending, $tree);
Loading history...
218
				break;
219
			default:
220
				$record = new self($xref, $gedcom, $pending, $tree);
0 ignored issues
show
Bug introduced by
It seems like $pending can also be of type stdClass; however, parameter $pending of Fisharebest\Webtrees\GedcomRecord::__construct() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

220
				$record = new self($xref, $gedcom, /** @scrutinizer ignore-type */ $pending, $tree);
Loading history...
221
				break;
222
		}
223
224
		// Store it in the cache
225
		self::$gedcom_record_cache[$xref][$tree_id] = $record;
226
227
		return $record;
228
	}
229
230
	/**
231
	 * Fetch data from the database
232
	 *
233
	 * @param string $xref
234
	 * @param int    $tree_id
235
	 *
236
	 * @return null|string
237
	 */
238
	protected static function fetchGedcomRecord($xref, $tree_id) {
239
		// We don't know what type of object this is. Try each one in turn.
240
		$data = Individual::fetchGedcomRecord($xref, $tree_id);
241
		if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
242
			return $data;
243
		}
244
		$data = Family::fetchGedcomRecord($xref, $tree_id);
245
		if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
246
			return $data;
247
		}
248
		$data = Source::fetchGedcomRecord($xref, $tree_id);
249
		if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
250
			return $data;
251
		}
252
		$data = Repository::fetchGedcomRecord($xref, $tree_id);
253
		if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
254
			return $data;
255
		}
256
		$data = Media::fetchGedcomRecord($xref, $tree_id);
257
		if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
258
			return $data;
259
		}
260
		$data = Note::fetchGedcomRecord($xref, $tree_id);
261
		if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
262
			return $data;
263
		}
264
		// Some other type of record...
265
266
		return Database::prepare(
267
			"SELECT o_gedcom FROM `##other` WHERE o_id = :xref AND o_file = :tree_id"
268
		)->execute([
269
			'xref'    => $xref,
270
			'tree_id' => $tree_id,
271
		])->fetchOne();
272
	}
273
274
	/**
275
	 * Get the XREF for this record
276
	 *
277
	 * @return string
278
	 */
279
	public function getXref() {
280
		return $this->xref;
281
	}
282
283
	/**
284
	 * Get the tree to which this record belongs
285
	 *
286
	 * @return Tree
287
	 */
288
	public function getTree() {
289
		return $this->tree;
290
	}
291
292
	/**
293
	 * Application code should access data via Fact objects.
294
	 * This function exists to support old code.
295
	 *
296
	 * @return string
297
	 */
298
	public function getGedcom() {
299
		if ($this->pending === null) {
300
			return $this->gedcom;
301
		} else {
302
			return $this->pending;
303
		}
304
	}
305
306
	/**
307
	 * Does this record have a pending change?
308
	 *
309
	 * @return bool
310
	 */
311
	public function isPendingAddition() {
312
		return $this->pending !== null;
313
	}
314
315
	/**
316
	 * Does this record have a pending deletion?
317
	 *
318
	 * @return bool
319
	 */
320
	public function isPendingDeletion() {
321
		return $this->pending === '';
322
	}
323
324
	/**
325
	 * Generate a URL to this record, suitable for use in HTML, etc.
326
	 *
327
	 * @deprecated
328
	 *
329
	 * @return string
330
	 */
331
	public function getHtmlUrl() {
332
		return e($this->url());
333
	}
334
335
	/**
336
	 * Generate a URL to this record.
337
	 *
338
	 * @deprecated
339
	 *
340
	 * @return string
341
	 */
342
	public function getRawUrl() {
343
		return $this->url();
344
	}
345
346
	/**
347
	 * Generate a URL to this record.
348
	 *
349
	 * @return string
350
	 */
351
	public function url() {
352
		return route(static::ROUTE_NAME, [
353
			'xref' => $this->getXref(),
354
			'ged'  => $this->tree->getName(),
355
		]);
356
	}
357
358
	/**
359
	 * Work out whether this record can be shown to a user with a given access level
360
	 *
361
	 * @param int $access_level
362
	 *
363
	 * @return bool
364
	 */
365
	private function canShowRecord($access_level) {
366
		// This setting would better be called "$ENABLE_PRIVACY"
367
		if (!$this->tree->getPreference('HIDE_LIVE_PEOPLE')) {
368
			return true;
369
		}
370
371
		// We should always be able to see our own record (unless an admin is applying download restrictions)
372
		if ($this->getXref() === $this->tree->getUserPreference(Auth::user(), 'gedcomid') && $access_level === Auth::accessLevel($this->tree)) {
373
			return true;
374
		}
375
376
		// Does this record have a RESN?
377
		if (strpos($this->gedcom, "\n1 RESN confidential")) {
378
			return Auth::PRIV_NONE >= $access_level;
379
		}
380
		if (strpos($this->gedcom, "\n1 RESN privacy")) {
381
			return Auth::PRIV_USER >= $access_level;
382
		}
383
		if (strpos($this->gedcom, "\n1 RESN none")) {
384
			return true;
385
		}
386
387
		// Does this record have a default RESN?
388
		$individual_privacy = $this->tree->getIndividualPrivacy();
389
		if (isset($individual_privacy[$this->getXref()])) {
390
			return $individual_privacy[$this->getXref()] >= $access_level;
391
		}
392
393
		// Privacy rules do not apply to admins
394
		if (Auth::PRIV_NONE >= $access_level) {
395
			return true;
396
		}
397
398
		// Different types of record have different privacy rules
399
		return $this->canShowByType($access_level);
400
	}
401
402
	/**
403
	 * Each object type may have its own special rules, and re-implement this function.
404
	 *
405
	 * @param int $access_level
406
	 *
407
	 * @return bool
408
	 */
409
	protected function canShowByType($access_level) {
410
		$fact_privacy = $this->tree->getFactPrivacy();
411
412
		if (isset($fact_privacy[static::RECORD_TYPE])) {
413
			// Restriction found
414
			return $fact_privacy[static::RECORD_TYPE] >= $access_level;
415
		} else {
416
			// No restriction found - must be public:
417
			return true;
418
		}
419
	}
420
421
	/**
422
	 * Can the details of this record be shown?
423
	 *
424
	 * @param int|null $access_level
425
	 *
426
	 * @return bool
427
	 */
428
	public function canShow($access_level = null) {
429
		if ($access_level === null) {
430
			$access_level = Auth::accessLevel($this->tree);
431
		}
432
433
		// CACHING: this function can take three different parameters,
434
		// and therefore needs three different caches for the result.
435
		switch ($access_level) {
436
			case Auth::PRIV_PRIVATE: // visitor
437
				if ($this->disp_public === null) {
0 ignored issues
show
introduced by
The condition $this->disp_public === null can never be true.
Loading history...
438
					$this->disp_public = $this->canShowRecord(Auth::PRIV_PRIVATE);
439
				}
440
441
				return $this->disp_public;
442
			case Auth::PRIV_USER: // member
443
				if ($this->disp_user === null) {
0 ignored issues
show
introduced by
The condition $this->disp_user === null can never be true.
Loading history...
444
					$this->disp_user = $this->canShowRecord(Auth::PRIV_USER);
445
				}
446
447
				return $this->disp_user;
448
			case Auth::PRIV_NONE: // admin
449
				if ($this->disp_none === null) {
0 ignored issues
show
introduced by
The condition $this->disp_none === null can never be true.
Loading history...
450
					$this->disp_none = $this->canShowRecord(Auth::PRIV_NONE);
451
				}
452
453
				return $this->disp_none;
454
			case Auth::PRIV_HIDE: // hidden from admins
455
				// We use this value to bypass privacy checks. For example,
456
				// when downloading data or when calculating privacy itself.
457
				return true;
458
			default:
459
				// Should never get here.
460
				return false;
461
		}
462
	}
463
464
	/**
465
	 * Can the name of this record be shown?
466
	 *
467
	 * @param int|null $access_level
468
	 *
469
	 * @return bool
470
	 */
471
	public function canShowName($access_level = null) {
472
		if ($access_level === null) {
473
			$access_level = Auth::accessLevel($this->tree);
474
		}
475
476
		return $this->canShow($access_level);
477
	}
478
479
	/**
480
	 * Can we edit this record?
481
	 *
482
	 * @return bool
483
	 */
484
	public function canEdit() {
485
		return Auth::isManager($this->tree) || Auth::isEditor($this->tree) && strpos($this->gedcom, "\n1 RESN locked") === false;
486
	}
487
488
	/**
489
	 * Remove private data from the raw gedcom record.
490
	 * Return both the visible and invisible data. We need the invisible data when editing.
491
	 *
492
	 * @param int $access_level
493
	 *
494
	 * @return string
495
	 */
496
	public function privatizeGedcom($access_level) {
497
		if ($access_level == Auth::PRIV_HIDE) {
498
			// We may need the original record, for example when downloading a GEDCOM or clippings cart
499
			return $this->gedcom;
500
		} elseif ($this->canShow($access_level)) {
501
			// The record is not private, but the individual facts may be.
502
503
			// Include the entire first line (for NOTE records)
504
			list($gedrec) = explode("\n", $this->gedcom, 2);
505
506
			// Check each of the facts for access
507
			foreach ($this->getFacts(null, false, $access_level) as $fact) {
508
				$gedrec .= "\n" . $fact->getGedcom();
509
			}
510
511
			return $gedrec;
512
		} else {
513
			// We cannot display the details, but we may be able to display
514
			// limited data, such as links to other records.
515
			return $this->createPrivateGedcomRecord($access_level);
516
		}
517
	}
518
519
	/**
520
	 * Generate a private version of this record
521
	 *
522
	 * @param int $access_level
523
	 *
524
	 * @return string
525
	 */
526
	protected function createPrivateGedcomRecord($access_level) {
527
		return '0 @' . $this->xref . '@ ' . static::RECORD_TYPE . "\n1 NOTE " . I18N::translate('Private');
528
	}
529
530
	/**
531
	 * Convert a name record into sortable and full/display versions. This default
532
	 * should be OK for simple record types. INDI/FAM records will need to redefine it.
533
	 *
534
	 * @param string $type
535
	 * @param string $value
536
	 * @param string $gedcom
537
	 */
538
	protected function addName($type, $value, $gedcom) {
539
		$this->_getAllNames[] = [
540
			'type'   => $type,
541
			'sort'   => preg_replace_callback('/([0-9]+)/', function ($matches) {
542
				return str_pad($matches[0], 10, '0', STR_PAD_LEFT);
543
			}, $value),
544
			'full'   => '<span dir="auto">' . e($value) . '</span>', // This is used for display
545
			'fullNN' => $value, // This goes into the database
546
		];
547
	}
548
549
	/**
550
	 * Get all the names of a record, including ROMN, FONE and _HEB alternatives.
551
	 * Records without a name (e.g. FAM) will need to redefine this function.
552
	 * Parameters: the level 1 fact containing the name.
553
	 * Return value: an array of name structures, each containing
554
	 * ['type'] = the gedcom fact, e.g. NAME, TITL, FONE, _HEB, etc.
555
	 * ['full'] = the name as specified in the record, e.g. 'Vincent van Gogh' or 'John Unknown'
556
	 * ['sort'] = a sortable version of the name (not for display), e.g. 'Gogh, Vincent' or '@N.N., John'
557
	 *
558
	 * @param int    $level
559
	 * @param string $fact_type
560
	 * @param Fact[] $facts
561
	 */
562
	protected function extractNamesFromFacts($level, $fact_type, $facts) {
563
		$sublevel    = $level + 1;
564
		$subsublevel = $sublevel + 1;
565
		foreach ($facts as $fact) {
566
			if (preg_match_all("/^{$level} ({$fact_type}) (.+)((\n[{$sublevel}-9].+)*)/m", $fact->getGedcom(), $matches, PREG_SET_ORDER)) {
567
				foreach ($matches as $match) {
568
					// Treat 1 NAME / 2 TYPE married the same as _MARNM
569
					if ($match[1] == 'NAME' && strpos($match[3], "\n2 TYPE married") !== false) {
570
						$this->addName('_MARNM', $match[2], $fact->getGedcom());
571
					} else {
572
						$this->addName($match[1], $match[2], $fact->getGedcom());
573
					}
574
					if ($match[3] && preg_match_all("/^{$sublevel} (ROMN|FONE|_\w+) (.+)((\n[{$subsublevel}-9].+)*)/m", $match[3], $submatches, PREG_SET_ORDER)) {
575
						foreach ($submatches as $submatch) {
576
							$this->addName($submatch[1], $submatch[2], $match[3]);
577
						}
578
					}
579
				}
580
			}
581
		}
582
	}
583
584
	/**
585
	 * Default for "other" object types
586
	 */
587
	public function extractNames() {
588
		$this->addName(static::RECORD_TYPE, $this->getFallBackName(), null);
589
	}
590
591
	/**
592
	 * Derived classes should redefine this function, otherwise the object will have no name
593
	 *
594
	 * @return string[][]
595
	 */
596
	public function getAllNames() {
597
		if ($this->_getAllNames === null) {
0 ignored issues
show
introduced by
The condition $this->_getAllNames === null can never be true.
Loading history...
598
			$this->_getAllNames = [];
599
			if ($this->canShowName()) {
600
				// Ask the record to extract its names
601
				$this->extractNames();
602
				// No name found? Use a fallback.
603
				if (!$this->_getAllNames) {
604
					$this->addName(static::RECORD_TYPE, $this->getFallBackName(), null);
605
				}
606
			} else {
607
				$this->addName(static::RECORD_TYPE, I18N::translate('Private'), null);
608
			}
609
		}
610
611
		return $this->_getAllNames;
612
	}
613
614
	/**
615
	 * If this object has no name, what do we call it?
616
	 *
617
	 * @return string
618
	 */
619
	public function getFallBackName() {
620
		return e($this->getXref());
621
	}
622
623
	/**
624
	 * Which of the (possibly several) names of this record is the primary one.
625
	 *
626
	 * @return int
627
	 */
628
	public function getPrimaryName() {
629
		static $language_script;
630
631
		if ($language_script === null) {
632
			$language_script = I18N::languageScript(WT_LOCALE);
633
		}
634
635
		if ($this->_getPrimaryName === null) {
0 ignored issues
show
introduced by
The condition $this->_getPrimaryName === null can never be true.
Loading history...
636
			// Generally, the first name is the primary one....
637
			$this->_getPrimaryName = 0;
638
			// ...except when the language/name use different character sets
639
			if (count($this->getAllNames()) > 1) {
640
				foreach ($this->getAllNames() as $n => $name) {
641
					if (I18N::textScript($name['sort']) === $language_script) {
642
						$this->_getPrimaryName = $n;
643
						break;
644
					}
645
				}
646
			}
647
		}
648
649
		return $this->_getPrimaryName;
650
	}
651
652
	/**
653
	 * Which of the (possibly several) names of this record is the secondary one.
654
	 *
655
	 * @return int
656
	 */
657
	public function getSecondaryName() {
658
		if (is_null($this->_getSecondaryName)) {
0 ignored issues
show
introduced by
The condition is_null($this->_getSecondaryName) can never be true.
Loading history...
659
			// Generally, the primary and secondary names are the same
660
			$this->_getSecondaryName = $this->getPrimaryName();
661
			// ....except when there are names with different character sets
662
			$all_names = $this->getAllNames();
663
			if (count($all_names) > 1) {
664
				$primary_script = I18N::textScript($all_names[$this->getPrimaryName()]['sort']);
665
				foreach ($all_names as $n => $name) {
666
					if ($n != $this->getPrimaryName() && $name['type'] != '_MARNM' && I18N::textScript($name['sort']) != $primary_script) {
667
						$this->_getSecondaryName = $n;
668
						break;
669
					}
670
				}
671
			}
672
		}
673
674
		return $this->_getSecondaryName;
675
	}
676
677
	/**
678
	 * Allow the choice of primary name to be overidden, e.g. in a search result
679
	 *
680
	 * @param int $n
681
	 */
682
	public function setPrimaryName($n) {
683
		$this->_getPrimaryName   = $n;
684
		$this->_getSecondaryName = null;
685
	}
686
687
	/**
688
	 * Allow native PHP functions such as array_unique() to work with objects
689
	 *
690
	 * @return string
691
	 */
692
	public function __toString() {
693
		return $this->xref . '@' . $this->tree->getTreeId();
694
	}
695
696
	/**
697
	 * Static helper function to sort an array of objects by name
698
	 * Records whose names cannot be displayed are sorted at the end.
699
	 *
700
	 * @param GedcomRecord $x
701
	 * @param GedcomRecord $y
702
	 *
703
	 * @return int
704
	 */
705
	public static function compare(GedcomRecord $x, GedcomRecord $y) {
706
		if ($x->canShowName()) {
707
			if ($y->canShowName()) {
708
				return I18N::strcasecmp($x->getSortName(), $y->getSortName());
709
			} else {
710
				return -1; // only $y is private
711
			}
712
		} else {
713
			if ($y->canShowName()) {
714
				return 1; // only $x is private
715
			} else {
716
				return 0; // both $x and $y private
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
717
			}
718
		}
719
	}
720
721
	/**
722
	 * Get variants of the name
723
	 *
724
	 * @return string
725
	 */
726
	public function getFullName() {
727
		if ($this->canShowName()) {
728
			$tmp = $this->getAllNames();
729
730
			return $tmp[$this->getPrimaryName()]['full'];
731
		} else {
732
			return I18N::translate('Private');
733
		}
734
	}
735
736
	/**
737
	 * Get a sortable version of the name. Do not display this!
738
	 *
739
	 * @return string
740
	 */
741
	public function getSortName() {
742
		// The sortable name is never displayed, no need to call canShowName()
743
		$tmp = $this->getAllNames();
744
745
		return $tmp[$this->getPrimaryName()]['sort'];
746
	}
747
748
	/**
749
	 * Get the full name in an alternative character set
750
	 *
751
	 * @return null|string
752
	 */
753
	public function getAddName() {
754
		if ($this->canShowName() && $this->getPrimaryName() != $this->getSecondaryName()) {
755
			$all_names = $this->getAllNames();
756
757
			return $all_names[$this->getSecondaryName()]['full'];
758
		} else {
759
			return null;
760
		}
761
	}
762
763
	/**
764
	 * Format this object for display in a list
765
	 *
766
	 * @return string
767
	 */
768
	public function formatList() {
769
		$html = '<a href="' . e($this->url()) . '" class="list_item">';
770
		$html .= '<b>' . $this->getFullName() . '</b>';
771
		$html .= $this->formatListDetails();
772
		$html .= '</a>';
773
774
		return $html;
775
	}
776
777
	/**
778
	 * This function should be redefined in derived classes to show any major
779
	 * identifying characteristics of this record.
780
	 *
781
	 * @return string
782
	 */
783
	public function formatListDetails() {
784
		return '';
785
	}
786
787
	/**
788
	 * Extract/format the first fact from a list of facts.
789
	 *
790
	 * @param string $facts
791
	 * @param int    $style
792
	 *
793
	 * @return string
794
	 */
795
	public function formatFirstMajorFact($facts, $style) {
796
		foreach ($this->getFacts($facts, true) as $event) {
797
			// Only display if it has a date or place (or both)
798
			if ($event->getDate()->isOK() && !$event->getPlace()->isEmpty()) {
799
				$joiner = ' — ';
800
			} else {
801
				$joiner = '';
802
			}
803
			if ($event->getDate()->isOK() || !$event->getPlace()->isEmpty()) {
804
				switch ($style) {
805
					case 1:
806
						return '<br><em>' . $event->getLabel() . ' ' . FunctionsPrint::formatFactDate($event, $this, false, false) . $joiner . FunctionsPrint::formatFactPlace($event) . '</em>';
807
					case 2:
808
						return '<dl><dt class="label">' . $event->getLabel() . '</dt><dd class="field">' . FunctionsPrint::formatFactDate($event, $this, false, false) . $joiner . FunctionsPrint::formatFactPlace($event) . '</dd></dl>';
809
				}
810
			}
811
		}
812
813
		return '';
814
	}
815
816
	/**
817
	 * Find individuals linked to this record.
818
	 *
819
	 * @param string $link
820
	 *
821
	 * @return Individual[]
822
	 */
823
	public function linkedIndividuals($link) {
824
		$rows = Database::prepare(
825
			"SELECT i_id AS xref, i_gedcom AS gedcom" .
826
			" FROM `##individuals`" .
827
			" JOIN `##link` ON i_file = l_file AND i_id = l_from" .
828
			" LEFT JOIN `##name` ON i_file = n_file AND i_id = n_id AND n_num = 0" .
829
			" WHERE i_file = :tree_id AND l_type = :link AND l_to = :xref" .
830
			" ORDER BY n_sort COLLATE :collation"
831
		)->execute([
832
			'tree_id'   => $this->tree->getTreeId(),
833
			'link'      => $link,
834
			'xref'      => $this->xref,
835
			'collation' => I18N::collation(),
836
		])->fetchAll();
837
838
		$list = [];
839
		foreach ($rows as $row) {
840
			$record = Individual::getInstance($row->xref, $this->tree, $row->gedcom);
841
			if ($record->canShowName()) {
842
				$list[] = $record;
843
			}
844
		}
845
846
		return $list;
847
	}
848
849
	/**
850
	 * Find families linked to this record.
851
	 *
852
	 * @param string $link
853
	 *
854
	 * @return Family[]
855
	 */
856
	public function linkedFamilies($link) {
857
		$rows = Database::prepare(
858
			"SELECT f_id AS xref, f_gedcom AS gedcom" .
859
			" FROM `##families`" .
860
			" JOIN `##link` ON f_file = l_file AND f_id = l_from" .
861
			" LEFT JOIN `##name` ON f_file = n_file AND f_id = n_id AND n_num = 0" .
862
			" WHERE f_file = :tree_id AND l_type = :link AND l_to = :xref"
863
		)->execute([
864
			'tree_id' => $this->tree->getTreeId(),
865
			'link'    => $link,
866
			'xref'    => $this->xref,
867
		])->fetchAll();
868
869
		$list = [];
870
		foreach ($rows as $row) {
871
			$record = Family::getInstance($row->xref, $this->tree, $row->gedcom);
872
			if ($record->canShowName()) {
873
				$list[] = $record;
874
			}
875
		}
876
877
		return $list;
878
	}
879
880
	/**
881
	 * Find sources linked to this record.
882
	 *
883
	 * @param string $link
884
	 *
885
	 * @return Source[]
886
	 */
887
	public function linkedSources($link) {
888
		$rows = Database::prepare(
889
			"SELECT s_id AS xref, s_gedcom AS gedcom" .
890
			" FROM `##sources`" .
891
			" JOIN `##link` ON s_file = l_file AND s_id = l_from" .
892
			" WHERE s_file = :tree_id AND l_type = :link AND l_to = :xref" .
893
			" ORDER BY s_name COLLATE :collation"
894
		)->execute([
895
			'tree_id'   => $this->tree->getTreeId(),
896
			'link'      => $link,
897
			'xref'      => $this->xref,
898
			'collation' => I18N::collation(),
899
		])->fetchAll();
900
901
		$list = [];
902
		foreach ($rows as $row) {
903
			$record = Source::getInstance($row->xref, $this->tree, $row->gedcom);
904
			if ($record->canShowName()) {
905
				$list[] = $record;
906
			}
907
		}
908
909
		return $list;
910
	}
911
912
	/**
913
	 * Find media objects linked to this record.
914
	 *
915
	 * @param string $link
916
	 *
917
	 * @return Media[]
918
	 */
919
	public function linkedMedia($link) {
920
		$rows = Database::prepare(
921
			"SELECT m_id AS xref, m_gedcom AS gedcom" .
922
			" FROM `##media`" .
923
			" JOIN `##link` ON m_file = l_file AND m_id = l_from" .
924
			" WHERE m_file = :tree_id AND l_type = :link AND l_to = :xref"
925
		)->execute([
926
			'tree_id'   => $this->tree->getTreeId(),
927
			'link'      => $link,
928
			'xref'      => $this->xref,
929
		])->fetchAll();
930
931
		$list = [];
932
		foreach ($rows as $row) {
933
			$record = Media::getInstance($row->xref, $this->tree, $row->gedcom);
934
			if ($record->canShowName()) {
935
				$list[] = $record;
936
			}
937
		}
938
939
		return $list;
940
	}
941
942
	/**
943
	 * Find notes linked to this record.
944
	 *
945
	 * @param string $link
946
	 *
947
	 * @return Note[]
948
	 */
949
	public function linkedNotes($link) {
950
		$rows = Database::prepare(
951
			"SELECT o_id AS xref, o_gedcom AS gedcom" .
952
			" FROM `##other`" .
953
			" JOIN `##link` ON o_file = l_file AND o_id = l_from" .
954
			" LEFT JOIN `##name` ON o_file = n_file AND o_id = n_id AND n_num = 0" .
955
			" WHERE o_file = :tree_id AND o_type = 'NOTE' AND l_type = :link AND l_to = :xref" .
956
			" ORDER BY n_sort COLLATE :collation"
957
		)->execute([
958
			'tree_id'   => $this->tree->getTreeId(),
959
			'link'      => $link,
960
			'xref'      => $this->xref,
961
			'collation' => I18N::collation(),
962
		])->fetchAll();
963
964
		$list = [];
965
		foreach ($rows as $row) {
966
			$record = Note::getInstance($row->xref, $this->tree, $row->gedcom);
967
			if ($record->canShowName()) {
968
				$list[] = $record;
969
			}
970
		}
971
972
		return $list;
973
	}
974
975
	/**
976
	 * Find repositories linked to this record.
977
	 *
978
	 * @param string $link
979
	 *
980
	 * @return Repository[]
981
	 */
982
	public function linkedRepositories($link) {
983
		$rows = Database::prepare(
984
			"SELECT o_id AS xref, o_gedcom AS gedcom" .
985
			" FROM `##other`" .
986
			" JOIN `##link` ON o_file = l_file AND o_id = l_from" .
987
			" LEFT JOIN `##name` ON o_file = n_file AND o_id = n_id AND n_num = 0" .
988
			" WHERE o_file = :tree_id AND o_type = 'REPO' AND l_type = :link AND l_to = :xref" .
989
			" ORDER BY n_sort COLLATE :collation"
990
		)->execute([
991
			'tree_id'   => $this->tree->getTreeId(),
992
			'link'      => $link,
993
			'xref'      => $this->xref,
994
			'collation' => I18N::collation(),
995
		])->fetchAll();
996
997
		$list = [];
998
		foreach ($rows as $row) {
999
			$record = Repository::getInstance($row->xref, $this->tree, $row->gedcom);
1000
			if ($record->canShowName()) {
1001
				$list[] = $record;
1002
			}
1003
		}
1004
1005
		return $list;
1006
	}
1007
1008
	/**
1009
	 * Get all attributes (e.g. DATE or PLAC) from an event (e.g. BIRT or MARR).
1010
	 * This is used to display multiple events on the individual/family lists.
1011
	 * Multiple events can exist because of uncertainty in dates, dates in different
1012
	 * calendars, place-names in both latin and hebrew character sets, etc.
1013
	 * It also allows us to combine dates/places from different events in the summaries.
1014
	 *
1015
	 * @param string $event_type
1016
	 *
1017
	 * @return Date[]
1018
	 */
1019
	public function getAllEventDates($event_type) {
1020
		$dates = [];
1021
		foreach ($this->getFacts($event_type) as $event) {
1022
			if ($event->getDate()->isOK()) {
1023
				$dates[] = $event->getDate();
1024
			}
1025
		}
1026
1027
		return $dates;
1028
	}
1029
1030
	/**
1031
	 * Get all the places for a particular type of event
1032
	 *
1033
	 * @param string $event_type
1034
	 *
1035
	 * @return Place[]
1036
	 */
1037
	public function getAllEventPlaces($event_type) {
1038
		$places = [];
1039
		foreach ($this->getFacts($event_type) as $event) {
1040
			if (preg_match_all('/\n(?:2 PLAC|3 (?:ROMN|FONE|_HEB)) +(.+)/', $event->getGedcom(), $ged_places)) {
1041
				foreach ($ged_places[1] as $ged_place) {
1042
					$places[] = new Place($ged_place, $this->tree);
1043
				}
1044
			}
1045
		}
1046
1047
		return $places;
1048
	}
1049
1050
	/**
1051
	 * Get the first (i.e. prefered) Fact for the given fact type
1052
	 *
1053
	 * @param string $tag
1054
	 *
1055
	 * @return Fact|null
1056
	 */
1057
	public function getFirstFact($tag) {
1058
		foreach ($this->getFacts() as $fact) {
1059
			if ($fact->getTag() === $tag) {
1060
				return $fact;
1061
			}
1062
		}
1063
1064
		return null;
1065
	}
1066
1067
	/**
1068
	 * The facts and events for this record.
1069
	 *
1070
	 * @param string    $filter
1071
	 * @param bool      $sort
1072
	 * @param int|null  $access_level
1073
	 * @param bool      $override     Include private records, to allow us to implement $SHOW_PRIVATE_RELATIONSHIPS and $SHOW_LIVING_NAMES.
1074
	 *
1075
	 * @return Fact[]
1076
	 */
1077
	public function getFacts($filter = null, $sort = false, $access_level = null, $override = false) {
1078
		if ($access_level === null) {
1079
			$access_level = Auth::accessLevel($this->tree);
1080
		}
1081
1082
		$facts = [];
1083
		if ($this->canShow($access_level) || $override) {
1084
			foreach ($this->facts as $fact) {
1085
				if (($filter === null || preg_match('/^' . $filter . '$/', $fact->getTag())) && $fact->canShow($access_level)) {
1086
					$facts[] = $fact;
1087
				}
1088
			}
1089
		}
1090
		if ($sort) {
1091
			Functions::sortFacts($facts);
1092
		}
1093
1094
		return $facts;
1095
	}
1096
1097
	/**
1098
	 * Get the last-change timestamp for this record, either as a formatted string
1099
	 * (for display) or as a unix timestamp (for sorting)
1100
	 *
1101
	 * @param bool $sorting
1102
	 *
1103
	 * @return string
1104
	 */
1105
	public function lastChangeTimestamp($sorting = false) {
1106
		$chan = $this->getFirstFact('CHAN');
1107
1108
		if ($chan) {
1109
			// The record does have a CHAN event
1110
			$d = $chan->getDate()->minimumDate();
1111
			if (preg_match('/\n3 TIME (\d\d):(\d\d):(\d\d)/', $chan->getGedcom(), $match)) {
1112
				$t = mktime((int) $match[1], (int) $match[2], (int) $match[3], (int) $d->format('%n'), (int) $d->format('%j'), (int) $d->format('%Y'));
1113
			} elseif (preg_match('/\n3 TIME (\d\d):(\d\d)/', $chan->getGedcom(), $match)) {
1114
				$t = mktime((int) $match[1], (int) $match[2], 0, (int) $d->format('%n'), (int) $d->format('%j'), (int) $d->format('%Y'));
1115
			} else {
1116
				$t = mktime(0, 0, 0, (int) $d->format('%n'), (int) $d->format('%j'), (int) $d->format('%Y'));
1117
			}
1118
			if ($sorting) {
1119
				return $t;
1120
			} else {
1121
				return strip_tags(FunctionsDate::formatTimestamp($t));
1122
			}
1123
		} else {
1124
			// The record does not have a CHAN event
1125
			if ($sorting) {
1126
				return '0';
1127
			} else {
1128
				return '';
1129
			}
1130
		}
1131
	}
1132
1133
	/**
1134
	 * Get the last-change user for this record
1135
	 *
1136
	 * @return string
1137
	 */
1138
	public function lastChangeUser() {
1139
		$chan = $this->getFirstFact('CHAN');
1140
1141
		if ($chan === null) {
1142
			return I18N::translate('Unknown');
1143
		} else {
1144
			$chan_user = $chan->getAttribute('_WT_USER');
1145
			if ($chan_user === '') {
1146
				return I18N::translate('Unknown');
1147
			} else {
1148
				return $chan_user;
1149
			}
1150
		}
1151
	}
1152
1153
	/**
1154
	 * Add a new fact to this record
1155
	 *
1156
	 * @param string $gedcom
1157
	 * @param bool   $update_chan
1158
	 */
1159
	public function createFact($gedcom, $update_chan) {
1160
		$this->updateFact(null, $gedcom, $update_chan);
1161
	}
1162
1163
	/**
1164
	 * Delete a fact from this record
1165
	 *
1166
	 * @param string $fact_id
1167
	 * @param bool   $update_chan
1168
	 */
1169
	public function deleteFact($fact_id, $update_chan) {
1170
		$this->updateFact($fact_id, null, $update_chan);
1171
	}
1172
1173
	/**
1174
	 * Replace a fact with a new gedcom data.
1175
	 *
1176
	 * @param string $fact_id
1177
	 * @param string $gedcom
1178
	 * @param bool   $update_chan
1179
	 *
1180
	 * @throws \Exception
1181
	 */
1182
	public function updateFact($fact_id, $gedcom, $update_chan) {
1183
		// MSDOS line endings will break things in horrible ways
1184
		$gedcom = preg_replace('/[\r\n]+/', "\n", $gedcom);
1185
		$gedcom = trim($gedcom);
1186
1187
		if ($this->pending === '') {
1188
			throw new \Exception('Cannot edit a deleted record');
1189
		}
1190
		if ($gedcom && !preg_match('/^1 ' . WT_REGEX_TAG . '/', $gedcom)) {
1191
			throw new \Exception('Invalid GEDCOM data passed to GedcomRecord::updateFact(' . $gedcom . ')');
1192
		}
1193
1194
		if ($this->pending) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->pending of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1195
			$old_gedcom = $this->pending;
1196
		} else {
1197
			$old_gedcom = $this->gedcom;
1198
		}
1199
1200
		// First line of record may contain data - e.g. NOTE records.
1201
		list($new_gedcom) = explode("\n", $old_gedcom, 2);
1202
1203
		// Replacing (or deleting) an existing fact
1204
		foreach ($this->getFacts(null, false, Auth::PRIV_HIDE) as $fact) {
1205
			if (!$fact->isPendingDeletion()) {
1206
				if ($fact->getFactId() === $fact_id) {
1207
					if ($gedcom) {
1208
						$new_gedcom .= "\n" . $gedcom;
1209
					}
1210
					$fact_id = true; // Only replace/delete one copy of a duplicate fact
1211
				} elseif ($fact->getTag() != 'CHAN' || !$update_chan) {
1212
					$new_gedcom .= "\n" . $fact->getGedcom();
1213
				}
1214
			}
1215
		}
1216
		if ($update_chan) {
1217
			$new_gedcom .= "\n1 CHAN\n2 DATE " . strtoupper(date('d M Y')) . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->getUserName();
1218
		}
1219
1220
		// Adding a new fact
1221
		if (!$fact_id) {
1222
			$new_gedcom .= "\n" . $gedcom;
1223
		}
1224
1225
		if ($new_gedcom != $old_gedcom) {
1226
			// Save the changes
1227
			Database::prepare(
1228
				"INSERT INTO `##change` (gedcom_id, xref, old_gedcom, new_gedcom, user_id) VALUES (?, ?, ?, ?, ?)"
1229
			)->execute([
1230
				$this->tree->getTreeId(),
1231
				$this->xref,
1232
				$old_gedcom,
1233
				$new_gedcom,
1234
				Auth::id(),
1235
			]);
1236
1237
			$this->pending = $new_gedcom;
1238
1239
			if (Auth::user()->getPreference('auto_accept')) {
1240
				FunctionsImport::acceptAllChanges($this->xref, $this->tree->getTreeId());
1241
				$this->gedcom  = $new_gedcom;
1242
				$this->pending = null;
1243
			}
1244
		}
1245
		$this->parseFacts();
1246
	}
1247
1248
	/**
1249
	 * Update this record
1250
	 *
1251
	 * @param string $gedcom
1252
	 * @param bool   $update_chan
1253
	 */
1254
	public function updateRecord($gedcom, $update_chan) {
1255
		// MSDOS line endings will break things in horrible ways
1256
		$gedcom = preg_replace('/[\r\n]+/', "\n", $gedcom);
1257
		$gedcom = trim($gedcom);
1258
1259
		// Update the CHAN record
1260
		if ($update_chan) {
1261
			$gedcom = preg_replace('/\n1 CHAN(\n[2-9].*)*/', '', $gedcom);
1262
			$gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->getUserName();
1263
		}
1264
1265
		// Create a pending change
1266
		Database::prepare(
1267
			"INSERT INTO `##change` (gedcom_id, xref, old_gedcom, new_gedcom, user_id) VALUES (?, ?, ?, ?, ?)"
1268
		)->execute([
1269
			$this->tree->getTreeId(),
1270
			$this->xref,
1271
			$this->getGedcom(),
1272
			$gedcom,
1273
			Auth::id(),
1274
		]);
1275
1276
		// Clear the cache
1277
		$this->pending = $gedcom;
1278
1279
		// Accept this pending change
1280
		if (Auth::user()->getPreference('auto_accept')) {
1281
			FunctionsImport::acceptAllChanges($this->xref, $this->tree->getTreeId());
1282
			$this->gedcom  = $gedcom;
1283
			$this->pending = null;
1284
		}
1285
1286
		$this->parseFacts();
1287
1288
		Log::addEditLog('Update: ' . static::RECORD_TYPE . ' ' . $this->xref);
1289
	}
1290
1291
	/**
1292
	 * Delete this record
1293
	 */
1294
	public function deleteRecord() {
1295
		// Create a pending change
1296
		if (!$this->isPendingDeletion()) {
1297
			Database::prepare(
1298
				"INSERT INTO `##change` (gedcom_id, xref, old_gedcom, new_gedcom, user_id) VALUES (?, ?, ?, '', ?)"
1299
			)->execute([
1300
				$this->tree->getTreeId(),
1301
				$this->xref,
1302
				$this->getGedcom(),
1303
				Auth::id(),
1304
			]);
1305
		}
1306
1307
		// Auto-accept this pending change
1308
		if (Auth::user()->getPreference('auto_accept')) {
1309
			FunctionsImport::acceptAllChanges($this->xref, $this->tree->getTreeId());
1310
		}
1311
1312
		// Clear the cache
1313
		self::$gedcom_record_cache  = null;
1314
		self::$pending_record_cache = null;
1315
1316
		Log::addEditLog('Delete: ' . static::RECORD_TYPE . ' ' . $this->xref);
1317
	}
1318
1319
	/**
1320
	 * Remove all links from this record to $xref
1321
	 *
1322
	 * @param string $xref
1323
	 * @param bool   $update_chan
1324
	 */
1325
	public function removeLinks($xref, $update_chan) {
1326
		$value = '@' . $xref . '@';
1327
1328
		foreach ($this->getFacts() as $fact) {
1329
			if ($fact->getValue() === $value) {
1330
				$this->deleteFact($fact->getFactId(), $update_chan);
1331
			} elseif (preg_match_all('/\n(\d) ' . WT_REGEX_TAG . ' ' . $value . '/', $fact->getGedcom(), $matches, PREG_SET_ORDER)) {
1332
				$gedcom = $fact->getGedcom();
1333
				foreach ($matches as $match) {
1334
					$next_level  = $match[1] + 1;
1335
					$next_levels = '[' . $next_level . '-9]';
1336
					$gedcom      = preg_replace('/' . $match[0] . '(\n' . $next_levels . '.*)*/', '', $gedcom);
1337
				}
1338
				$this->updateFact($fact->getFactId(), $gedcom, $update_chan);
1339
			}
1340
		}
1341
	}
1342
}
1343