Passed
Branch master (60084b)
by Greg
10:38
created

GedcomRecord::deleteRecord()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 4
nop 0
dl 0
loc 23
rs 9.0856
c 0
b 0
f 0
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
	 * If $find is set, then we are displaying items from a selection list.
766
	 * $name allows us to use something other than the record name.
767
	 *
768
	 * @param string $tag
769
	 * @param bool   $find
770
	 * @param null   $name
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $name is correct as it would always require null to be passed?
Loading history...
771
	 *
772
	 * @return string
773
	 */
774
	public function formatList($tag = 'li', $find = false, $name = null) {
775
		if (is_null($name)) {
776
			$name = $this->getFullName();
777
		}
778
		$html = '<a href="' . e($this->url()) . '"';
779
		if ($find) {
780
			$html .= ' onclick="pasteid(\'' . $this->getXref() . '\', \'' . htmlentities($name) . '\');"';
781
		}
782
		$html .= ' class="list_item"><b>' . $name . '</b>';
783
		$html .= $this->formatListDetails();
784
		$html = '<' . $tag . '>' . $html . '</a></' . $tag . '>';
785
786
		return $html;
787
	}
788
789
	/**
790
	 * This function should be redefined in derived classes to show any major
791
	 * identifying characteristics of this record.
792
	 *
793
	 * @return string
794
	 */
795
	public function formatListDetails() {
796
		return '';
797
	}
798
799
	/**
800
	 * Extract/format the first fact from a list of facts.
801
	 *
802
	 * @param string $facts
803
	 * @param int    $style
804
	 *
805
	 * @return string
806
	 */
807
	public function formatFirstMajorFact($facts, $style) {
808
		foreach ($this->getFacts($facts, true) as $event) {
809
			// Only display if it has a date or place (or both)
810
			if ($event->getDate()->isOK() && !$event->getPlace()->isEmpty()) {
811
				$joiner = ' — ';
812
			} else {
813
				$joiner = '';
814
			}
815
			if ($event->getDate()->isOK() || !$event->getPlace()->isEmpty()) {
816
				switch ($style) {
817
					case 1:
818
						return '<br><em>' . $event->getLabel() . ' ' . FunctionsPrint::formatFactDate($event, $this, false, false) . $joiner . FunctionsPrint::formatFactPlace($event) . '</em>';
819
					case 2:
820
						return '<dl><dt class="label">' . $event->getLabel() . '</dt><dd class="field">' . FunctionsPrint::formatFactDate($event, $this, false, false) . $joiner . FunctionsPrint::formatFactPlace($event) . '</dd></dl>';
821
				}
822
			}
823
		}
824
825
		return '';
826
	}
827
828
	/**
829
	 * Find individuals linked to this record.
830
	 *
831
	 * @param string $link
832
	 *
833
	 * @return Individual[]
834
	 */
835
	public function linkedIndividuals($link) {
836
		$rows = Database::prepare(
837
			"SELECT i_id AS xref, i_gedcom AS gedcom" .
838
			" FROM `##individuals`" .
839
			" JOIN `##link` ON i_file = l_file AND i_id = l_from" .
840
			" LEFT JOIN `##name` ON i_file = n_file AND i_id = n_id AND n_num = 0" .
841
			" WHERE i_file = :tree_id AND l_type = :link AND l_to = :xref" .
842
			" ORDER BY n_sort COLLATE :collation"
843
		)->execute([
844
			'tree_id'   => $this->tree->getTreeId(),
845
			'link'      => $link,
846
			'xref'      => $this->xref,
847
			'collation' => I18N::collation(),
848
		])->fetchAll();
849
850
		$list = [];
851
		foreach ($rows as $row) {
852
			$record = Individual::getInstance($row->xref, $this->tree, $row->gedcom);
853
			if ($record->canShowName()) {
854
				$list[] = $record;
855
			}
856
		}
857
858
		return $list;
859
	}
860
861
	/**
862
	 * Find families linked to this record.
863
	 *
864
	 * @param string $link
865
	 *
866
	 * @return Family[]
867
	 */
868
	public function linkedFamilies($link) {
869
		$rows = Database::prepare(
870
			"SELECT f_id AS xref, f_gedcom AS gedcom" .
871
			" FROM `##families`" .
872
			" JOIN `##link` ON f_file = l_file AND f_id = l_from" .
873
			" LEFT JOIN `##name` ON f_file = n_file AND f_id = n_id AND n_num = 0" .
874
			" WHERE f_file = :tree_id AND l_type = :link AND l_to = :xref"
875
		)->execute([
876
			'tree_id' => $this->tree->getTreeId(),
877
			'link'    => $link,
878
			'xref'    => $this->xref,
879
		])->fetchAll();
880
881
		$list = [];
882
		foreach ($rows as $row) {
883
			$record = Family::getInstance($row->xref, $this->tree, $row->gedcom);
884
			if ($record->canShowName()) {
885
				$list[] = $record;
886
			}
887
		}
888
889
		return $list;
890
	}
891
892
	/**
893
	 * Find sources linked to this record.
894
	 *
895
	 * @param string $link
896
	 *
897
	 * @return Source[]
898
	 */
899
	public function linkedSources($link) {
900
		$rows = Database::prepare(
901
			"SELECT s_id AS xref, s_gedcom AS gedcom" .
902
			" FROM `##sources`" .
903
			" JOIN `##link` ON s_file = l_file AND s_id = l_from" .
904
			" WHERE s_file = :tree_id AND l_type = :link AND l_to = :xref" .
905
			" ORDER BY s_name COLLATE :collation"
906
		)->execute([
907
			'tree_id'   => $this->tree->getTreeId(),
908
			'link'      => $link,
909
			'xref'      => $this->xref,
910
			'collation' => I18N::collation(),
911
		])->fetchAll();
912
913
		$list = [];
914
		foreach ($rows as $row) {
915
			$record = Source::getInstance($row->xref, $this->tree, $row->gedcom);
916
			if ($record->canShowName()) {
917
				$list[] = $record;
918
			}
919
		}
920
921
		return $list;
922
	}
923
924
	/**
925
	 * Find media objects linked to this record.
926
	 *
927
	 * @param string $link
928
	 *
929
	 * @return Media[]
930
	 */
931
	public function linkedMedia($link) {
932
		$rows = Database::prepare(
933
			"SELECT m_id AS xref, m_gedcom AS gedcom" .
934
			" FROM `##media`" .
935
			" JOIN `##link` ON m_file = l_file AND m_id = l_from" .
936
			" WHERE m_file = :tree_id AND l_type = :link AND l_to = :xref"
937
		)->execute([
938
			'tree_id'   => $this->tree->getTreeId(),
939
			'link'      => $link,
940
			'xref'      => $this->xref,
941
		])->fetchAll();
942
943
		$list = [];
944
		foreach ($rows as $row) {
945
			$record = Media::getInstance($row->xref, $this->tree, $row->gedcom);
946
			if ($record->canShowName()) {
947
				$list[] = $record;
948
			}
949
		}
950
951
		return $list;
952
	}
953
954
	/**
955
	 * Find notes linked to this record.
956
	 *
957
	 * @param string $link
958
	 *
959
	 * @return Note[]
960
	 */
961
	public function linkedNotes($link) {
962
		$rows = Database::prepare(
963
			"SELECT o_id AS xref, o_gedcom AS gedcom" .
964
			" FROM `##other`" .
965
			" JOIN `##link` ON o_file = l_file AND o_id = l_from" .
966
			" LEFT JOIN `##name` ON o_file = n_file AND o_id = n_id AND n_num = 0" .
967
			" WHERE o_file = :tree_id AND o_type = 'NOTE' AND l_type = :link AND l_to = :xref" .
968
			" ORDER BY n_sort COLLATE :collation"
969
		)->execute([
970
			'tree_id'   => $this->tree->getTreeId(),
971
			'link'      => $link,
972
			'xref'      => $this->xref,
973
			'collation' => I18N::collation(),
974
		])->fetchAll();
975
976
		$list = [];
977
		foreach ($rows as $row) {
978
			$record = Note::getInstance($row->xref, $this->tree, $row->gedcom);
979
			if ($record->canShowName()) {
980
				$list[] = $record;
981
			}
982
		}
983
984
		return $list;
985
	}
986
987
	/**
988
	 * Find repositories linked to this record.
989
	 *
990
	 * @param string $link
991
	 *
992
	 * @return Repository[]
993
	 */
994
	public function linkedRepositories($link) {
995
		$rows = Database::prepare(
996
			"SELECT o_id AS xref, o_gedcom AS gedcom" .
997
			" FROM `##other`" .
998
			" JOIN `##link` ON o_file = l_file AND o_id = l_from" .
999
			" LEFT JOIN `##name` ON o_file = n_file AND o_id = n_id AND n_num = 0" .
1000
			" WHERE o_file = :tree_id AND o_type = 'REPO' AND l_type = :link AND l_to = :xref" .
1001
			" ORDER BY n_sort COLLATE :collation"
1002
		)->execute([
1003
			'tree_id'   => $this->tree->getTreeId(),
1004
			'link'      => $link,
1005
			'xref'      => $this->xref,
1006
			'collation' => I18N::collation(),
1007
		])->fetchAll();
1008
1009
		$list = [];
1010
		foreach ($rows as $row) {
1011
			$record = Repository::getInstance($row->xref, $this->tree, $row->gedcom);
1012
			if ($record->canShowName()) {
1013
				$list[] = $record;
1014
			}
1015
		}
1016
1017
		return $list;
1018
	}
1019
1020
	/**
1021
	 * Get all attributes (e.g. DATE or PLAC) from an event (e.g. BIRT or MARR).
1022
	 * This is used to display multiple events on the individual/family lists.
1023
	 * Multiple events can exist because of uncertainty in dates, dates in different
1024
	 * calendars, place-names in both latin and hebrew character sets, etc.
1025
	 * It also allows us to combine dates/places from different events in the summaries.
1026
	 *
1027
	 * @param string $event_type
1028
	 *
1029
	 * @return Date[]
1030
	 */
1031
	public function getAllEventDates($event_type) {
1032
		$dates = [];
1033
		foreach ($this->getFacts($event_type) as $event) {
1034
			if ($event->getDate()->isOK()) {
1035
				$dates[] = $event->getDate();
1036
			}
1037
		}
1038
1039
		return $dates;
1040
	}
1041
1042
	/**
1043
	 * Get all the places for a particular type of event
1044
	 *
1045
	 * @param string $event_type
1046
	 *
1047
	 * @return Place[]
1048
	 */
1049
	public function getAllEventPlaces($event_type) {
1050
		$places = [];
1051
		foreach ($this->getFacts($event_type) as $event) {
1052
			if (preg_match_all('/\n(?:2 PLAC|3 (?:ROMN|FONE|_HEB)) +(.+)/', $event->getGedcom(), $ged_places)) {
1053
				foreach ($ged_places[1] as $ged_place) {
1054
					$places[] = new Place($ged_place, $this->tree);
1055
				}
1056
			}
1057
		}
1058
1059
		return $places;
1060
	}
1061
1062
	/**
1063
	 * Get the first (i.e. prefered) Fact for the given fact type
1064
	 *
1065
	 * @param string $tag
1066
	 *
1067
	 * @return Fact|null
1068
	 */
1069
	public function getFirstFact($tag) {
1070
		foreach ($this->getFacts() as $fact) {
1071
			if ($fact->getTag() === $tag) {
1072
				return $fact;
1073
			}
1074
		}
1075
1076
		return null;
1077
	}
1078
1079
	/**
1080
	 * The facts and events for this record.
1081
	 *
1082
	 * @param string    $filter
1083
	 * @param bool      $sort
1084
	 * @param int|null  $access_level
1085
	 * @param bool      $override     Include private records, to allow us to implement $SHOW_PRIVATE_RELATIONSHIPS and $SHOW_LIVING_NAMES.
1086
	 *
1087
	 * @return Fact[]
1088
	 */
1089
	public function getFacts($filter = null, $sort = false, $access_level = null, $override = false) {
1090
		if ($access_level === null) {
1091
			$access_level = Auth::accessLevel($this->tree);
1092
		}
1093
1094
		$facts = [];
1095
		if ($this->canShow($access_level) || $override) {
1096
			foreach ($this->facts as $fact) {
1097
				if (($filter === null || preg_match('/^' . $filter . '$/', $fact->getTag())) && $fact->canShow($access_level)) {
1098
					$facts[] = $fact;
1099
				}
1100
			}
1101
		}
1102
		if ($sort) {
1103
			Functions::sortFacts($facts);
1104
		}
1105
1106
		return $facts;
1107
	}
1108
1109
	/**
1110
	 * Get the last-change timestamp for this record, either as a formatted string
1111
	 * (for display) or as a unix timestamp (for sorting)
1112
	 *
1113
	 * @param bool $sorting
1114
	 *
1115
	 * @return string
1116
	 */
1117
	public function lastChangeTimestamp($sorting = false) {
1118
		$chan = $this->getFirstFact('CHAN');
1119
1120
		if ($chan) {
1121
			// The record does have a CHAN event
1122
			$d = $chan->getDate()->minimumDate();
1123
			if (preg_match('/\n3 TIME (\d\d):(\d\d):(\d\d)/', $chan->getGedcom(), $match)) {
1124
				$t = mktime((int) $match[1], (int) $match[2], (int) $match[3], (int) $d->format('%n'), (int) $d->format('%j'), (int) $d->format('%Y'));
1125
			} elseif (preg_match('/\n3 TIME (\d\d):(\d\d)/', $chan->getGedcom(), $match)) {
1126
				$t = mktime((int) $match[1], (int) $match[2], 0, (int) $d->format('%n'), (int) $d->format('%j'), (int) $d->format('%Y'));
1127
			} else {
1128
				$t = mktime(0, 0, 0, (int) $d->format('%n'), (int) $d->format('%j'), (int) $d->format('%Y'));
1129
			}
1130
			if ($sorting) {
1131
				return $t;
1132
			} else {
1133
				return strip_tags(FunctionsDate::formatTimestamp($t));
1134
			}
1135
		} else {
1136
			// The record does not have a CHAN event
1137
			if ($sorting) {
1138
				return '0';
1139
			} else {
1140
				return '';
1141
			}
1142
		}
1143
	}
1144
1145
	/**
1146
	 * Get the last-change user for this record
1147
	 *
1148
	 * @return string
1149
	 */
1150
	public function lastChangeUser() {
1151
		$chan = $this->getFirstFact('CHAN');
1152
1153
		if ($chan === null) {
1154
			return I18N::translate('Unknown');
1155
		} else {
1156
			$chan_user = $chan->getAttribute('_WT_USER');
1157
			if ($chan_user === '') {
1158
				return I18N::translate('Unknown');
1159
			} else {
1160
				return $chan_user;
1161
			}
1162
		}
1163
	}
1164
1165
	/**
1166
	 * Add a new fact to this record
1167
	 *
1168
	 * @param string $gedcom
1169
	 * @param bool   $update_chan
1170
	 */
1171
	public function createFact($gedcom, $update_chan) {
1172
		$this->updateFact(null, $gedcom, $update_chan);
1173
	}
1174
1175
	/**
1176
	 * Delete a fact from this record
1177
	 *
1178
	 * @param string $fact_id
1179
	 * @param bool   $update_chan
1180
	 */
1181
	public function deleteFact($fact_id, $update_chan) {
1182
		$this->updateFact($fact_id, null, $update_chan);
1183
	}
1184
1185
	/**
1186
	 * Replace a fact with a new gedcom data.
1187
	 *
1188
	 * @param string $fact_id
1189
	 * @param string $gedcom
1190
	 * @param bool   $update_chan
1191
	 *
1192
	 * @throws \Exception
1193
	 */
1194
	public function updateFact($fact_id, $gedcom, $update_chan) {
1195
		// MSDOS line endings will break things in horrible ways
1196
		$gedcom = preg_replace('/[\r\n]+/', "\n", $gedcom);
1197
		$gedcom = trim($gedcom);
1198
1199
		if ($this->pending === '') {
1200
			throw new \Exception('Cannot edit a deleted record');
1201
		}
1202
		if ($gedcom && !preg_match('/^1 ' . WT_REGEX_TAG . '/', $gedcom)) {
1203
			throw new \Exception('Invalid GEDCOM data passed to GedcomRecord::updateFact(' . $gedcom . ')');
1204
		}
1205
1206
		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...
1207
			$old_gedcom = $this->pending;
1208
		} else {
1209
			$old_gedcom = $this->gedcom;
1210
		}
1211
1212
		// First line of record may contain data - e.g. NOTE records.
1213
		list($new_gedcom) = explode("\n", $old_gedcom, 2);
1214
1215
		// Replacing (or deleting) an existing fact
1216
		foreach ($this->getFacts(null, false, Auth::PRIV_HIDE) as $fact) {
1217
			if (!$fact->isPendingDeletion()) {
1218
				if ($fact->getFactId() === $fact_id) {
1219
					if ($gedcom) {
1220
						$new_gedcom .= "\n" . $gedcom;
1221
					}
1222
					$fact_id = true; // Only replace/delete one copy of a duplicate fact
1223
				} elseif ($fact->getTag() != 'CHAN' || !$update_chan) {
1224
					$new_gedcom .= "\n" . $fact->getGedcom();
1225
				}
1226
			}
1227
		}
1228
		if ($update_chan) {
1229
			$new_gedcom .= "\n1 CHAN\n2 DATE " . strtoupper(date('d M Y')) . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->getUserName();
1230
		}
1231
1232
		// Adding a new fact
1233
		if (!$fact_id) {
1234
			$new_gedcom .= "\n" . $gedcom;
1235
		}
1236
1237
		if ($new_gedcom != $old_gedcom) {
1238
			// Save the changes
1239
			Database::prepare(
1240
				"INSERT INTO `##change` (gedcom_id, xref, old_gedcom, new_gedcom, user_id) VALUES (?, ?, ?, ?, ?)"
1241
			)->execute([
1242
				$this->tree->getTreeId(),
1243
				$this->xref,
1244
				$old_gedcom,
1245
				$new_gedcom,
1246
				Auth::id(),
1247
			]);
1248
1249
			$this->pending = $new_gedcom;
1250
1251
			if (Auth::user()->getPreference('auto_accept')) {
1252
				FunctionsImport::acceptAllChanges($this->xref, $this->tree->getTreeId());
1253
				$this->gedcom  = $new_gedcom;
1254
				$this->pending = null;
1255
			}
1256
		}
1257
		$this->parseFacts();
1258
	}
1259
1260
	/**
1261
	 * Update this record
1262
	 *
1263
	 * @param string $gedcom
1264
	 * @param bool   $update_chan
1265
	 */
1266
	public function updateRecord($gedcom, $update_chan) {
1267
		// MSDOS line endings will break things in horrible ways
1268
		$gedcom = preg_replace('/[\r\n]+/', "\n", $gedcom);
1269
		$gedcom = trim($gedcom);
1270
1271
		// Update the CHAN record
1272
		if ($update_chan) {
1273
			$gedcom = preg_replace('/\n1 CHAN(\n[2-9].*)*/', '', $gedcom);
1274
			$gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->getUserName();
1275
		}
1276
1277
		// Create a pending change
1278
		Database::prepare(
1279
			"INSERT INTO `##change` (gedcom_id, xref, old_gedcom, new_gedcom, user_id) VALUES (?, ?, ?, ?, ?)"
1280
		)->execute([
1281
			$this->tree->getTreeId(),
1282
			$this->xref,
1283
			$this->getGedcom(),
1284
			$gedcom,
1285
			Auth::id(),
1286
		]);
1287
1288
		// Clear the cache
1289
		$this->pending = $gedcom;
1290
1291
		// Accept this pending change
1292
		if (Auth::user()->getPreference('auto_accept')) {
1293
			FunctionsImport::acceptAllChanges($this->xref, $this->tree->getTreeId());
1294
			$this->gedcom  = $gedcom;
1295
			$this->pending = null;
1296
		}
1297
1298
		$this->parseFacts();
1299
1300
		Log::addEditLog('Update: ' . static::RECORD_TYPE . ' ' . $this->xref);
1301
	}
1302
1303
	/**
1304
	 * Delete this record
1305
	 */
1306
	public function deleteRecord() {
1307
		// Create a pending change
1308
		if (!$this->isPendingDeletion()) {
1309
			Database::prepare(
1310
				"INSERT INTO `##change` (gedcom_id, xref, old_gedcom, new_gedcom, user_id) VALUES (?, ?, ?, '', ?)"
1311
			)->execute([
1312
				$this->tree->getTreeId(),
1313
				$this->xref,
1314
				$this->getGedcom(),
1315
				Auth::id(),
1316
			]);
1317
		}
1318
1319
		// Auto-accept this pending change
1320
		if (Auth::user()->getPreference('auto_accept')) {
1321
			FunctionsImport::acceptAllChanges($this->xref, $this->tree->getTreeId());
1322
		}
1323
1324
		// Clear the cache
1325
		self::$gedcom_record_cache  = null;
1326
		self::$pending_record_cache = null;
1327
1328
		Log::addEditLog('Delete: ' . static::RECORD_TYPE . ' ' . $this->xref);
1329
	}
1330
1331
	/**
1332
	 * Remove all links from this record to $xref
1333
	 *
1334
	 * @param string $xref
1335
	 * @param bool   $update_chan
1336
	 */
1337
	public function removeLinks($xref, $update_chan) {
1338
		$value = '@' . $xref . '@';
1339
1340
		foreach ($this->getFacts() as $fact) {
1341
			if ($fact->getValue() === $value) {
1342
				$this->deleteFact($fact->getFactId(), $update_chan);
1343
			} elseif (preg_match_all('/\n(\d) ' . WT_REGEX_TAG . ' ' . $value . '/', $fact->getGedcom(), $matches, PREG_SET_ORDER)) {
1344
				$gedcom = $fact->getGedcom();
1345
				foreach ($matches as $match) {
1346
					$next_level  = $match[1] + 1;
1347
					$next_levels = '[' . $next_level . '-9]';
1348
					$gedcom      = preg_replace('/' . $match[0] . '(\n' . $next_levels . '.*)*/', '', $gedcom);
1349
				}
1350
				$this->updateFact($fact->getFactId(), $gedcom, $update_chan);
1351
			}
1352
		}
1353
	}
1354
}
1355