Test Failed
Branch master (4a3c5b)
by Greg
12:31
created

Tree   F

Complexity

Total Complexity 70

Size/Duplication

Total Lines 731
Duplicated Lines 2.6 %

Importance

Changes 0
Metric Value
dl 19
loc 731
rs 2.1818
c 0
b 0
f 0
wmc 70

28 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 29 4
A findByName() 0 8 3
A getNameList() 7 7 2
A getNameHtml() 0 2 1
B setUserPreference() 0 28 3
A getTreeId() 0 2 1
A getNewXref() 0 48 3
A getPreference() 0 11 3
A getFactPrivacy() 0 2 1
A getName() 0 2 1
A getTitleHtml() 0 2 1
B exportGedcom() 0 30 3
A delete() 0 21 2
A findById() 0 7 3
A canAcceptChanges() 0 2 1
A getIdList() 7 7 2
C importGedcomFile() 0 39 7
B createRecord() 3 43 6
A getIndividualPrivacy() 0 2 1
B getAll() 0 27 3
C create() 0 88 8
A getIndividualFactPrivacy() 0 2 1
A getTitle() 0 2 1
A deleteGenealogyData() 0 17 2
A setPreference() 0 17 2
A getNameUrl() 0 2 1
A hasPendingEdit() 0 6 1
A getUserPreference() 0 13 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Tree 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 Tree, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2017 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\FunctionsExport;
19
use Fisharebest\Webtrees\Functions\FunctionsImport;
20
use PDOException;
21
22
/**
23
 * Provide an interface to the wt_gedcom table.
24
 */
25
class Tree {
26
	/** @var int The tree's ID number */
27
	private $tree_id;
28
29
	/** @var string The tree's name */
30
	private $name;
31
32
	/** @var string The tree's title */
33
	private $title;
34
35
	/** @var int[] Default access rules for facts in this tree */
36
	private $fact_privacy;
37
38
	/** @var int[] Default access rules for individuals in this tree */
39
	private $individual_privacy;
40
41
	/** @var integer[][] Default access rules for individual facts in this tree */
42
	private $individual_fact_privacy;
43
44
	/** @var Tree[] All trees that we have permission to see. */
45
	private static $trees;
46
47
	/** @var string[] Cached copy of the wt_gedcom_setting table. */
48
	private $preferences = [];
49
50
	/** @var string[][] Cached copy of the wt_user_gedcom_setting table. */
51
	private $user_preferences = [];
52
53
	/**
54
	 * Create a tree object. This is a private constructor - it can only
55
	 * be called from Tree::getAll() to ensure proper initialisation.
56
	 *
57
	 * @param int    $tree_id
58
	 * @param string $tree_name
59
	 * @param string $tree_title
60
	 */
61
	private function __construct($tree_id, $tree_name, $tree_title) {
62
		$this->tree_id                 = $tree_id;
63
		$this->name                    = $tree_name;
64
		$this->title                   = $tree_title;
65
		$this->fact_privacy            = [];
66
		$this->individual_privacy      = [];
67
		$this->individual_fact_privacy = [];
68
69
		// Load the privacy settings for this tree
70
		$rows = Database::prepare(
71
			"SELECT SQL_CACHE xref, tag_type, CASE resn WHEN 'none' THEN :priv_public WHEN 'privacy' THEN :priv_user WHEN 'confidential' THEN :priv_none WHEN 'hidden' THEN :priv_hide END AS resn" .
72
			" FROM `##default_resn` WHERE gedcom_id = :tree_id"
73
		)->execute([
74
			'priv_public' => Auth::PRIV_PRIVATE,
75
			'priv_user'   => Auth::PRIV_USER,
76
			'priv_none'   => Auth::PRIV_NONE,
77
			'priv_hide'   => Auth::PRIV_HIDE,
78
			'tree_id'     => $this->tree_id,
79
		])->fetchAll();
80
81
		foreach ($rows as $row) {
82
			if ($row->xref !== null) {
83
				if ($row->tag_type !== null) {
84
					$this->individual_fact_privacy[$row->xref][$row->tag_type] = (int) $row->resn;
85
				} else {
86
					$this->individual_privacy[$row->xref] = (int) $row->resn;
87
				}
88
			} else {
89
				$this->fact_privacy[$row->tag_type] = (int) $row->resn;
90
			}
91
		}
92
	}
93
94
	/**
95
	 * The ID of this tree
96
	 *
97
	 * @return int
98
	 */
99
	public function getTreeId() {
100
		return $this->tree_id;
101
	}
102
103
	/**
104
	 * The name of this tree
105
	 *
106
	 * @return string
107
	 */
108
	public function getName() {
109
		return $this->name;
110
	}
111
112
	/**
113
	 * The name of this tree
114
	 *
115
	 * @return string
116
	 */
117
	public function getNameHtml() {
118
		return Html::escape($this->name);
119
	}
120
121
	/**
122
	 * The name of this tree
123
	 *
124
	 * @return string
125
	 */
126
	public function getNameUrl() {
127
		return rawurlencode($this->name);
128
	}
129
130
	/**
131
	 * The title of this tree
132
	 *
133
	 * @return string
134
	 */
135
	public function getTitle() {
136
		return $this->title;
137
	}
138
139
	/**
140
	 * The title of this tree, with HTML markup
141
	 *
142
	 * @return string
143
	 */
144
	public function getTitleHtml() {
145
		return '<span dir="auto">' . Html::escape($this->title) . '</span>';
146
	}
147
148
	/**
149
	 * The fact-level privacy for this tree.
150
	 *
151
	 * @return int[]
152
	 */
153
	public function getFactPrivacy() {
154
		return $this->fact_privacy;
155
	}
156
157
	/**
158
	 * The individual-level privacy for this tree.
159
	 *
160
	 * @return int[]
161
	 */
162
	public function getIndividualPrivacy() {
163
		return $this->individual_privacy;
164
	}
165
166
	/**
167
	 * The individual-fact-level privacy for this tree.
168
	 *
169
	 * @return integer[][]
170
	 */
171
	public function getIndividualFactPrivacy() {
172
		return $this->individual_fact_privacy;
173
	}
174
175
	/**
176
	 * Get the tree’s configuration settings.
177
	 *
178
	 * @param string $setting_name
179
	 * @param string $default
180
	 *
181
	 * @return string
182
	 */
183
	public function getPreference($setting_name, $default = '') {
184
		if (empty($this->preferences)) {
185
			$this->preferences = Database::prepare(
186
				"SELECT SQL_CACHE setting_name, setting_value FROM `##gedcom_setting` WHERE gedcom_id = ?"
187
			)->execute([$this->tree_id])->fetchAssoc();
188
		}
189
190
		if (array_key_exists($setting_name, $this->preferences)) {
191
			return $this->preferences[$setting_name];
192
		} else {
193
			return $default;
194
		}
195
	}
196
197
	/**
198
	 * Set the tree’s configuration settings.
199
	 *
200
	 * @param string $setting_name
201
	 * @param string $setting_value
202
	 *
203
	 * @return $this
204
	 */
205
	public function setPreference($setting_name, $setting_value) {
206
		if ($setting_value !== $this->getPreference($setting_name)) {
207
			Database::prepare(
208
				"REPLACE INTO `##gedcom_setting` (gedcom_id, setting_name, setting_value)" .
209
				" VALUES (:tree_id, :setting_name, LEFT(:setting_value, 255))"
210
			)->execute([
211
				'tree_id'       => $this->tree_id,
212
				'setting_name'  => $setting_name,
213
				'setting_value' => $setting_value,
214
			]);
215
216
			$this->preferences[$setting_name] = $setting_value;
217
218
			Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '"', $this);
219
		}
220
221
		return $this;
222
	}
223
224
	/**
225
	 * Get the tree’s user-configuration settings.
226
	 *
227
	 * @param User        $user
228
	 * @param string      $setting_name
229
	 * @param string|null $default
230
	 *
231
	 * @return string
232
	 */
233
	public function getUserPreference(User $user, $setting_name, $default = null) {
234
		// There are lots of settings, and we need to fetch lots of them on every page
235
		// so it is quicker to fetch them all in one go.
236
		if (!array_key_exists($user->getUserId(), $this->user_preferences)) {
237
			$this->user_preferences[$user->getUserId()] = Database::prepare(
238
				"SELECT SQL_CACHE setting_name, setting_value FROM `##user_gedcom_setting` WHERE user_id = ? AND gedcom_id = ?"
239
			)->execute([$user->getUserId(), $this->tree_id])->fetchAssoc();
240
		}
241
242
		if (array_key_exists($setting_name, $this->user_preferences[$user->getUserId()])) {
243
			return $this->user_preferences[$user->getUserId()][$setting_name];
244
		} else {
245
			return $default;
246
		}
247
	}
248
249
	/**
250
	 * Set the tree’s user-configuration settings.
251
	 *
252
	 * @param User    $user
253
	 * @param string  $setting_name
254
	 * @param string  $setting_value
255
	 *
256
	 * @return $this
257
	 */
258
	public function setUserPreference(User $user, $setting_name, $setting_value) {
259
		if ($this->getUserPreference($user, $setting_name) !== $setting_value) {
260
			// Update the database
261
			if ($setting_value === null) {
262
				Database::prepare(
263
					"DELETE FROM `##user_gedcom_setting` WHERE gedcom_id = :tree_id AND user_id = :user_id AND setting_name = :setting_name"
264
				)->execute([
265
					'tree_id'      => $this->tree_id,
266
					'user_id'      => $user->getUserId(),
267
					'setting_name' => $setting_name,
268
				]);
269
			} else {
270
				Database::prepare(
271
					"REPLACE INTO `##user_gedcom_setting` (user_id, gedcom_id, setting_name, setting_value) VALUES (:user_id, :tree_id, :setting_name, LEFT(:setting_value, 255))"
272
				)->execute([
273
					'user_id'       => $user->getUserId(),
274
					'tree_id'       => $this->tree_id,
275
					'setting_name'  => $setting_name,
276
					'setting_value' => $setting_value,
277
				]);
278
			}
279
			// Update our cache
280
			$this->user_preferences[$user->getUserId()][$setting_name] = $setting_value;
281
			// Audit log of changes
282
			Log::addConfigurationLog('Tree preference "' . $setting_name . '" set to "' . $setting_value . '" for user "' . $user->getUserName() . '"', $this);
283
		}
284
285
		return $this;
286
	}
287
288
	/**
289
	 * Can a user accept changes for this tree?
290
	 *
291
	 * @param User $user
292
	 *
293
	 * @return bool
294
	 */
295
	public function canAcceptChanges(User $user) {
296
		return Auth::isModerator($this, $user);
297
	}
298
299
	/**
300
	 * Fetch all the trees that we have permission to access.
301
	 *
302
	 * @return Tree[]
303
	 */
304
	public static function getAll() {
305
		if (self::$trees === null) {
306
			self::$trees = [];
307
			$rows        = Database::prepare(
308
				"SELECT SQL_CACHE g.gedcom_id AS tree_id, g.gedcom_name AS tree_name, gs1.setting_value AS tree_title" .
309
				" FROM `##gedcom` g" .
310
				" LEFT JOIN `##gedcom_setting`      gs1 ON (g.gedcom_id=gs1.gedcom_id AND gs1.setting_name='title')" .
311
				" LEFT JOIN `##gedcom_setting`      gs2 ON (g.gedcom_id=gs2.gedcom_id AND gs2.setting_name='imported')" .
312
				" LEFT JOIN `##gedcom_setting`      gs3 ON (g.gedcom_id=gs3.gedcom_id AND gs3.setting_name='REQUIRE_AUTHENTICATION')" .
313
				" LEFT JOIN `##user_gedcom_setting` ugs ON (g.gedcom_id=ugs.gedcom_id AND ugs.setting_name='canedit' AND ugs.user_id=?)" .
314
				" WHERE " .
315
				"  g.gedcom_id>0 AND (" . // exclude the "template" tree
316
				"    EXISTS (SELECT 1 FROM `##user_setting` WHERE user_id=? AND setting_name='canadmin' AND setting_value=1)" . // Admin sees all
317
				"   ) OR (" .
318
				"    (gs2.setting_value = 1 OR ugs.setting_value = 'admin') AND (" . // Allow imported trees, with either:
319
				"     gs3.setting_value <> 1 OR" . // visitor access
320
				"     IFNULL(ugs.setting_value, 'none')<>'none'" . // explicit access
321
				"   )" .
322
				"  )" .
323
				" ORDER BY g.sort_order, 3"
324
			)->execute([Auth::id(), Auth::id()])->fetchAll();
325
			foreach ($rows as $row) {
326
				self::$trees[] = new self((int) $row->tree_id, $row->tree_name, $row->tree_title);
327
			}
328
		}
329
330
		return self::$trees;
331
	}
332
333
	/**
334
	 * Find the tree with a specific ID.
335
	 *
336
	 * @param int $tree_id
337
	 *
338
	 * @throws \DomainException
339
	 *
340
	 * @return Tree
341
	 */
342
	public static function findById($tree_id) {
343
		foreach (self::getAll() as $tree) {
344
			if ($tree->tree_id == $tree_id) {
345
				return $tree;
346
			}
347
		}
348
		throw new \DomainException;
349
	}
350
351
	/**
352
	 * Find the tree with a specific name.
353
	 *
354
	 * @param string $tree_name
355
	 *
356
	 * @return Tree|null
357
	 */
358
	public static function findByName($tree_name) {
359
		foreach (self::getAll() as $tree) {
360
			if ($tree->name === $tree_name) {
361
				return $tree;
362
			}
363
		}
364
365
		return null;
366
	}
367
368
	/**
369
	 * Create arguments to select_edit_control()
370
	 * Note - these will be escaped later
371
	 *
372
	 * @return string[]
373
	 */
374 View Code Duplication
	public static function getIdList() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
375
		$list = [];
376
		foreach (self::getAll() as $tree) {
377
			$list[$tree->tree_id] = $tree->title;
378
		}
379
380
		return $list;
381
	}
382
383
	/**
384
	 * Create arguments to select_edit_control()
385
	 * Note - these will be escaped later
386
	 *
387
	 * @return string[]
388
	 */
389 View Code Duplication
	public static function getNameList() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
390
		$list = [];
391
		foreach (self::getAll() as $tree) {
392
			$list[$tree->name] = $tree->title;
393
		}
394
395
		return $list;
396
	}
397
398
	/**
399
	 * Create a new tree
400
	 *
401
	 * @param string $tree_name
402
	 * @param string $tree_title
403
	 *
404
	 * @return Tree
405
	 */
406
	public static function create($tree_name, $tree_title) {
407
		try {
408
			// Create a new tree
409
			Database::prepare(
410
				"INSERT INTO `##gedcom` (gedcom_name) VALUES (?)"
411
			)->execute([$tree_name]);
412
			$tree_id = Database::prepare("SELECT LAST_INSERT_ID()")->fetchOne();
413
		} catch (PDOException $ex) {
414
			DebugBar::addThrowable($ex);
415
416
			// A tree with that name already exists?
417
			return self::findByName($tree_name);
418
		}
419
420
		// Update the list of trees - to include this new one
421
		self::$trees = null;
422
		$tree        = self::findById($tree_id);
0 ignored issues
show
Bug introduced by
It seems like $tree_id can also be of type string; however, parameter $tree_id of Fisharebest\Webtrees\Tree::findById() does only seem to accept integer, 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

422
		$tree        = self::findById(/** @scrutinizer ignore-type */ $tree_id);
Loading history...
423
424
		$tree->setPreference('imported', '0');
425
		$tree->setPreference('title', $tree_title);
426
427
		// Module privacy
428
		Module::setDefaultAccess($tree_id);
0 ignored issues
show
Bug introduced by
It seems like $tree_id can also be of type string; however, parameter $tree_id of Fisharebest\Webtrees\Module::setDefaultAccess() does only seem to accept integer, 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

428
		Module::setDefaultAccess(/** @scrutinizer ignore-type */ $tree_id);
Loading history...
429
430
		// Set preferences from default tree
431
		Database::prepare(
432
			"INSERT INTO `##gedcom_setting` (gedcom_id, setting_name, setting_value)" .
433
			" SELECT :tree_id, setting_name, setting_value" .
434
			" FROM `##gedcom_setting` WHERE gedcom_id = -1"
435
		)->execute([
436
			'tree_id' => $tree_id,
437
		]);
438
439
		Database::prepare(
440
			"INSERT INTO `##default_resn` (gedcom_id, tag_type, resn)" .
441
			" SELECT :tree_id, tag_type, resn" .
442
			" FROM `##default_resn` WHERE gedcom_id = -1"
443
		)->execute([
444
			'tree_id' => $tree_id,
445
		]);
446
447
		Database::prepare(
448
			"INSERT INTO `##block` (gedcom_id, location, block_order, module_name)" .
449
			" SELECT :tree_id, location, block_order, module_name" .
450
			" FROM `##block` WHERE gedcom_id = -1"
451
		)->execute([
452
			'tree_id' => $tree_id,
453
		]);
454
455
		// Gedcom and privacy settings
456
		$tree->setPreference('CONTACT_USER_ID', Auth::id());
457
		$tree->setPreference('WEBMASTER_USER_ID', Auth::id());
458
		$tree->setPreference('LANGUAGE', WT_LOCALE); // Default to the current admin’s language
459
		switch (WT_LOCALE) {
460
		case 'es':
461
			$tree->setPreference('SURNAME_TRADITION', 'spanish');
462
			break;
463
		case 'is':
464
			$tree->setPreference('SURNAME_TRADITION', 'icelandic');
465
			break;
466
		case 'lt':
467
			$tree->setPreference('SURNAME_TRADITION', 'lithuanian');
468
			break;
469
		case 'pl':
470
			$tree->setPreference('SURNAME_TRADITION', 'polish');
471
			break;
472
		case 'pt':
473
		case 'pt-BR':
474
			$tree->setPreference('SURNAME_TRADITION', 'portuguese');
475
			break;
476
		default:
477
			$tree->setPreference('SURNAME_TRADITION', 'paternal');
478
			break;
479
		}
480
481
		// Genealogy data
482
		// It is simpler to create a temporary/unimported GEDCOM than to populate all the tables...
483
		$john_doe = /* I18N: This should be a common/default/placeholder name of an individual. Put slashes around the surname. */ I18N::translate('John /DOE/');
484
		$note     = I18N::translate('Edit this individual and replace their details with your own.');
485
		Database::prepare("INSERT INTO `##gedcom_chunk` (gedcom_id, chunk_data) VALUES (?, ?)")->execute([
486
			$tree_id,
487
			"0 HEAD\n1 CHAR UTF-8\n0 @I1@ INDI\n1 NAME {$john_doe}\n1 SEX M\n1 BIRT\n2 DATE 01 JAN 1850\n2 NOTE {$note}\n0 TRLR\n",
488
		]);
489
490
		// Update our cache
491
		self::$trees[$tree->tree_id] = $tree;
492
493
		return $tree;
494
	}
495
496
	/**
497
	 * Are there any pending edits for this tree, than need reviewing by a moderator.
498
	 *
499
	 * @return bool
500
	 */
501
	public function hasPendingEdit() {
502
		return (bool) Database::prepare(
503
			"SELECT 1 FROM `##change` WHERE status = 'pending' AND gedcom_id = :tree_id"
504
		)->execute([
505
			'tree_id' => $this->tree_id,
506
		])->fetchOne();
507
	}
508
509
	/**
510
	 * Delete all the genealogy data from a tree - in preparation for importing
511
	 * new data. Optionally retain the media data, for when the user has been
512
	 * editing their data offline using an application which deletes (or does not
513
	 * support) media data.
514
	 *
515
	 * @param bool $keep_media
516
	 */
517
	public function deleteGenealogyData($keep_media) {
518
		Database::prepare("DELETE FROM `##gedcom_chunk` WHERE gedcom_id = ?")->execute([$this->tree_id]);
519
		Database::prepare("DELETE FROM `##individuals`  WHERE i_file    = ?")->execute([$this->tree_id]);
520
		Database::prepare("DELETE FROM `##families`     WHERE f_file    = ?")->execute([$this->tree_id]);
521
		Database::prepare("DELETE FROM `##sources`      WHERE s_file    = ?")->execute([$this->tree_id]);
522
		Database::prepare("DELETE FROM `##other`        WHERE o_file    = ?")->execute([$this->tree_id]);
523
		Database::prepare("DELETE FROM `##places`       WHERE p_file    = ?")->execute([$this->tree_id]);
524
		Database::prepare("DELETE FROM `##placelinks`   WHERE pl_file   = ?")->execute([$this->tree_id]);
525
		Database::prepare("DELETE FROM `##name`         WHERE n_file    = ?")->execute([$this->tree_id]);
526
		Database::prepare("DELETE FROM `##dates`        WHERE d_file    = ?")->execute([$this->tree_id]);
527
		Database::prepare("DELETE FROM `##change`       WHERE gedcom_id = ?")->execute([$this->tree_id]);
528
529
		if ($keep_media) {
530
			Database::prepare("DELETE FROM `##link` WHERE l_file =? AND l_type<>'OBJE'")->execute([$this->tree_id]);
531
		} else {
532
			Database::prepare("DELETE FROM `##link`  WHERE l_file =?")->execute([$this->tree_id]);
533
			Database::prepare("DELETE FROM `##media` WHERE m_file =?")->execute([$this->tree_id]);
534
		}
535
	}
536
537
	/**
538
	 * Delete everything relating to a tree
539
	 */
540
	public function delete() {
541
		// If this is the default tree, then unset it
542
		if (Site::getPreference('DEFAULT_GEDCOM') === $this->name) {
543
			Site::setPreference('DEFAULT_GEDCOM', '');
544
		}
545
546
		$this->deleteGenealogyData(false);
547
548
		Database::prepare("DELETE `##block_setting` FROM `##block_setting` JOIN `##block` USING (block_id) WHERE gedcom_id=?")->execute([$this->tree_id]);
549
		Database::prepare("DELETE FROM `##block`               WHERE gedcom_id = ?")->execute([$this->tree_id]);
550
		Database::prepare("DELETE FROM `##user_gedcom_setting` WHERE gedcom_id = ?")->execute([$this->tree_id]);
551
		Database::prepare("DELETE FROM `##gedcom_setting`      WHERE gedcom_id = ?")->execute([$this->tree_id]);
552
		Database::prepare("DELETE FROM `##module_privacy`      WHERE gedcom_id = ?")->execute([$this->tree_id]);
553
		Database::prepare("DELETE FROM `##hit_counter`         WHERE gedcom_id = ?")->execute([$this->tree_id]);
554
		Database::prepare("DELETE FROM `##default_resn`        WHERE gedcom_id = ?")->execute([$this->tree_id]);
555
		Database::prepare("DELETE FROM `##gedcom_chunk`        WHERE gedcom_id = ?")->execute([$this->tree_id]);
556
		Database::prepare("DELETE FROM `##log`                 WHERE gedcom_id = ?")->execute([$this->tree_id]);
557
		Database::prepare("DELETE FROM `##gedcom`              WHERE gedcom_id = ?")->execute([$this->tree_id]);
558
559
		// After updating the database, we need to fetch a new (sorted) copy
560
		self::$trees = null;
561
	}
562
563
	/**
564
	 * Export the tree to a GEDCOM file
565
	 *
566
	 * @param resource $stream
567
	 */
568
	public function exportGedcom($stream) {
569
		$stmt = Database::prepare(
570
			"SELECT i_gedcom AS gedcom, i_id AS xref, 1 AS n FROM `##individuals` WHERE i_file = :tree_id_1" .
571
			" UNION ALL " .
572
			"SELECT f_gedcom AS gedcom, f_id AS xref, 2 AS n FROM `##families`    WHERE f_file = :tree_id_2" .
573
			" UNION ALL " .
574
			"SELECT s_gedcom AS gedcom, s_id AS xref, 3 AS n FROM `##sources`     WHERE s_file = :tree_id_3" .
575
			" UNION ALL " .
576
			"SELECT o_gedcom AS gedcom, o_id AS xref, 4 AS n FROM `##other`       WHERE o_file = :tree_id_4 AND o_type NOT IN ('HEAD', 'TRLR')" .
577
			" UNION ALL " .
578
			"SELECT m_gedcom AS gedcom, m_id AS xref, 5 AS n FROM `##media`       WHERE m_file = :tree_id_5" .
579
			" ORDER BY n, LENGTH(xref), xref"
580
		)->execute([
581
			'tree_id_1' => $this->tree_id,
582
			'tree_id_2' => $this->tree_id,
583
			'tree_id_3' => $this->tree_id,
584
			'tree_id_4' => $this->tree_id,
585
			'tree_id_5' => $this->tree_id,
586
		]);
587
588
		$buffer = FunctionsExport::reformatRecord(FunctionsExport::gedcomHeader($this));
589
		while (($row = $stmt->fetch()) !== false) {
590
			$buffer .= FunctionsExport::reformatRecord($row->gedcom);
591
			if (strlen($buffer) > 65535) {
592
				fwrite($stream, $buffer);
593
				$buffer = '';
594
			}
595
		}
596
		fwrite($stream, $buffer . '0 TRLR' . WT_EOL);
597
		$stmt->closeCursor();
598
	}
599
600
	/**
601
	 * Import data from a gedcom file into this tree.
602
	 *
603
	 * @param string  $path       The full path to the (possibly temporary) file.
604
	 * @param string  $filename   The preferred filename, for export/download.
605
	 *
606
	 * @throws \Exception
607
	 */
608
	public function importGedcomFile($path, $filename) {
609
		// Read the file in blocks of roughly 64K. Ensure that each block
610
		// contains complete gedcom records. This will ensure we don’t split
611
		// multi-byte characters, as well as simplifying the code to import
612
		// each block.
613
614
		$file_data = '';
615
		$fp        = fopen($path, 'rb');
616
617
		// Don’t allow the user to cancel the request. We do not want to be left with an incomplete transaction.
618
		ignore_user_abort(true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $value of ignore_user_abort(). ( Ignorable by Annotation )

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

618
		ignore_user_abort(/** @scrutinizer ignore-type */ true);
Loading history...
619
620
		Database::beginTransaction();
621
		$this->deleteGenealogyData($this->getPreference('keep_media'));
0 ignored issues
show
Bug introduced by
$this->getPreference('keep_media') of type string is incompatible with the type boolean expected by parameter $keep_media of Fisharebest\Webtrees\Tree::deleteGenealogyData(). ( Ignorable by Annotation )

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

621
		$this->deleteGenealogyData(/** @scrutinizer ignore-type */ $this->getPreference('keep_media'));
Loading history...
622
		$this->setPreference('gedcom_filename', $filename);
623
		$this->setPreference('imported', '0');
624
625
		while (!feof($fp)) {
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of feof() does only seem to accept resource, 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

625
		while (!feof(/** @scrutinizer ignore-type */ $fp)) {
Loading history...
626
			$file_data .= fread($fp, 65536);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fread() does only seem to accept resource, 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

626
			$file_data .= fread(/** @scrutinizer ignore-type */ $fp, 65536);
Loading history...
627
			// There is no strrpos() function that searches for substrings :-(
628
			for ($pos = strlen($file_data) - 1; $pos > 0; --$pos) {
629
				if ($file_data[$pos] === '0' && ($file_data[$pos - 1] === "\n" || $file_data[$pos - 1] === "\r")) {
630
					// We’ve found the last record boundary in this chunk of data
631
					break;
632
				}
633
			}
634
			if ($pos) {
635
				Database::prepare(
636
					"INSERT INTO `##gedcom_chunk` (gedcom_id, chunk_data) VALUES (?, ?)"
637
				)->execute([$this->tree_id, substr($file_data, 0, $pos)]);
638
				$file_data = substr($file_data, $pos);
639
			}
640
		}
641
		Database::prepare(
642
			"INSERT INTO `##gedcom_chunk` (gedcom_id, chunk_data) VALUES (?, ?)"
643
		)->execute([$this->tree_id, $file_data]);
644
645
		Database::commit();
646
		fclose($fp);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, 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

646
		fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
647
	}
648
649
	/**
650
	 * Generate a new XREF, unique across all family trees
651
	 *
652
	 * @return string
653
	 */
654
	public function getNewXref() {
655
		$prefix = 'X';
656
657
		$increment = 1.0;
658
		do {
659
			// Use LAST_INSERT_ID(expr) to provide a transaction-safe sequence. See
660
			// http://dev.mysql.com/doc/refman/5.6/en/information-functions.html#function_last-insert-id
661
			$statement = Database::prepare(
662
				"UPDATE `##site_setting` SET setting_value = LAST_INSERT_ID(setting_value + :increment) WHERE setting_name = 'next_xref'"
663
			);
664
			$statement->execute([
665
				'increment'   => (int) $increment,
666
			]);
667
668
			if ($statement->rowCount() === 0) {
669
				// First time we've used this record type.
670
				Site::setPreference('next_xref', '1');
671
				$num = 1;
672
			} else {
673
				$num = Database::prepare("SELECT LAST_INSERT_ID()")->fetchOne();
674
			}
675
676
			$xref = $prefix . $num;
677
678
			// Records may already exist with this sequence number.
679
			$already_used = Database::prepare(
680
				"SELECT" .
681
				" EXISTS (SELECT 1 FROM `##individuals` WHERE i_id = :i_id) OR" .
682
				" EXISTS (SELECT 1 FROM `##families` WHERE f_id = :f_id) OR" .
683
				" EXISTS (SELECT 1 FROM `##sources` WHERE s_id = :s_id) OR" .
684
				" EXISTS (SELECT 1 FROM `##media` WHERE m_id = :m_id) OR" .
685
				" EXISTS (SELECT 1 FROM `##other` WHERE o_id = :o_id) OR" .
686
				" EXISTS (SELECT 1 FROM `##change` WHERE xref = :xref)"
687
			)->execute([
688
				'i_id' => $xref,
689
				'f_id' => $xref,
690
				's_id' => $xref,
691
				'm_id' => $xref,
692
				'o_id' => $xref,
693
				'xref' => $xref,
694
			])->fetchOne();
695
696
			// This exponential increment allows us to scan over large blocks of
697
			// existing data in a reasonable time.
698
			$increment *= 1.01;
699
		} while ($already_used !== '0');
700
701
		return $xref;
702
	}
703
704
	/**
705
	 * Create a new record from GEDCOM data.
706
	 *
707
	 * @param string $gedcom
708
	 *
709
	 * @throws \Exception
710
	 *
711
	 * @return GedcomRecord|Individual|Family|Note|Source|Repository|Media
712
	 */
713
	public function createRecord($gedcom) {
714
		if (preg_match('/^0 @(' . WT_REGEX_XREF . ')@ (' . WT_REGEX_TAG . ')/', $gedcom, $match)) {
715
			$xref = $match[1];
716
			$type = $match[2];
717
		} else {
718
			throw new \Exception('Invalid argument to GedcomRecord::createRecord(' . $gedcom . ')');
719
		}
720
		if (strpos("\r", $gedcom) !== false) {
721
			// MSDOS line endings will break things in horrible ways
722
			throw new \Exception('Evil line endings found in GedcomRecord::createRecord(' . $gedcom . ')');
723
		}
724
725
		// webtrees creates XREFs containing digits. Anything else (e.g. “new”) is just a placeholder.
726
		if (!preg_match('/\d/', $xref)) {
727
			$xref   = $this->getNewXref();
728
			$gedcom = preg_replace('/^0 @(' . WT_REGEX_XREF . ')@/', '0 @' . $xref . '@', $gedcom);
729
		}
730
731
		// Create a change record, if not already present
732 View Code Duplication
		if (!preg_match('/\n1 CHAN/', $gedcom)) {
733
			$gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->getUserName();
0 ignored issues
show
Bug introduced by
Are you sure date('H:i:s') of type false|string 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

733
			$gedcom .= "\n1 CHAN\n2 DATE " . date('d M Y') . "\n3 TIME " . /** @scrutinizer ignore-type */ date('H:i:s') . "\n2 _WT_USER " . Auth::user()->getUserName();
Loading history...
Bug introduced by
Are you sure date('d M Y') of type false|string 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

733
			$gedcom .= "\n1 CHAN\n2 DATE " . /** @scrutinizer ignore-type */ date('d M Y') . "\n3 TIME " . date('H:i:s') . "\n2 _WT_USER " . Auth::user()->getUserName();
Loading history...
734
		}
735
736
		// Create a pending change
737
		Database::prepare(
738
			"INSERT INTO `##change` (gedcom_id, xref, old_gedcom, new_gedcom, user_id) VALUES (?, ?, '', ?, ?)"
739
		)->execute([
740
			$this->tree_id,
741
			$xref,
742
			$gedcom,
743
			Auth::id(),
744
		]);
745
746
		Log::addEditLog('Create: ' . $type . ' ' . $xref);
747
748
		// Accept this pending change
749
		if (Auth::user()->getPreference('auto_accept')) {
750
			FunctionsImport::acceptAllChanges($xref, $this->tree_id);
751
		}
752
		// Return the newly created record. Note that since GedcomRecord
753
		// has a cache of pending changes, we cannot use it to create a
754
		// record with a newly created pending change.
755
		return GedcomRecord::getInstance($xref, $this, $gedcom);
756
	}
757
}
758