Completed
Push — openstreetmap ( c02ae5...190f01 )
by Greg
17:40 queued 08:16
created

AdminTreesController::places()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 14
nc 2
nop 1
dl 0
loc 21
rs 9.3142
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
declare(strict_types=1);
17
18
namespace Fisharebest\Webtrees\Http\Controllers;
19
20
use Fisharebest\Algorithm\ConnectedComponent;
21
use Fisharebest\Webtrees\Database;
22
use Fisharebest\Webtrees\DebugBar;
23
use Fisharebest\Webtrees\Family;
24
use Fisharebest\Webtrees\FlashMessages;
25
use Fisharebest\Webtrees\Functions\Functions;
26
use Fisharebest\Webtrees\Functions\FunctionsExport;
27
use Fisharebest\Webtrees\Html;
28
use Fisharebest\Webtrees\I18N;
29
use Fisharebest\Webtrees\Individual;
30
use Fisharebest\Webtrees\Media;
31
use Fisharebest\Webtrees\Site;
32
use Fisharebest\Webtrees\Tree;
33
use Fisharebest\Webtrees\User;
34
use League\Flysystem\Filesystem;
35
use League\Flysystem\ZipArchive\ZipArchiveAdapter;
36
use Symfony\Component\HttpFoundation\BinaryFileResponse;
37
use Symfony\Component\HttpFoundation\RedirectResponse;
38
use Symfony\Component\HttpFoundation\Request;
39
use Symfony\Component\HttpFoundation\Response;
40
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
41
use Symfony\Component\HttpFoundation\StreamedResponse;
42
use Throwable;
43
44
/**
45
 * Controller for tree administration.
46
 */
47
class AdminTreesController extends AbstractBaseController {
48
	// Show a reduced page when there are more than a certain number of trees
49
	const MULTIPLE_TREE_THRESHOLD = 500;
50
51
	protected $layout = 'layouts/administration';
52
53
	/**
54
	 * @param Request $request
55
	 *
56
	 * @return RedirectResponse
57
	 */
58
	public function create(Request $request): RedirectResponse {
59
		/** @var Tree $tree */
60
		$tree = $request->attributes->get('tree');
61
62
		$tree_name  = $request->get('tree_name', '');
63
		$tree_title = $request->get('tree_title', '');
64
65
		// We use the tree name as a file name, so no directory separators allowed.
66
		$tree_name = basename($tree_name);
67
68
		if ($tree_name !== '' && $tree_title !== '') {
69
			if (Tree::findByName($tree_name)) {
70
				FlashMessages::addMessage(I18N::translate('The family tree “%s” already exists.', e($tree_name)), 'danger');
71
			} else {
72
				$tree = Tree::create($tree_name, $tree_title);
73
				FlashMessages::addMessage(I18N::translate('The family tree “%s” has been created.', e($tree->getName())), 'success');
74
			}
75
		}
76
77
		$url = route('admin-trees', ['ged' => $tree->getName()]);
78
79
		return new RedirectResponse($url);
80
	}
81
82
	/**
83
	 * @param Request $request
84
	 *
85
	 * @return RedirectResponse
86
	 */
87
	public function delete(Request $request): RedirectResponse {
88
		/** @var Tree $tree */
89
		$tree = $request->attributes->get('tree');
90
91
		FlashMessages::addMessage(/* I18N: %s is the name of a family tree */
92
			I18N::translate('The family tree “%s” has been deleted.', e($tree->getTitle())), 'success');
93
94
		$tree->delete();
95
96
		$url = route('admin-trees');
97
98
		return new RedirectResponse($url);
99
	}
100
101
	/**
102
	 * @param Request $request
103
	 *
104
	 * @return Response
105
	 */
106
	public function export(Request $request): Response {
107
		/** @var Tree $tree */
108
		$tree = $request->attributes->get('tree');
109
110
		$title = I18N::translate('Export a GEDCOM file') . ' — ' . e($tree->getTitle());
111
112
		return $this->viewResponse('admin/trees-export', [
113
			'title' => $title,
114
			'tree'  => $tree,
115
		]);
116
	}
117
118
	/**
119
	 * @param Request $request
120
	 *
121
	 * @return Response
122
	 */
123
	public function exportClient(Request $request): Response {
124
		/** @var Tree $tree */
125
		$tree = $request->attributes->get('tree');
126
127
		// Validate user parameters
128
		$convert          = (bool) $request->get('convert');
129
		$zip              = (bool) $request->get('zip');
130
		$media            = (bool) $request->get('media');
131
		$media_path       = $request->get('media-path');
132
		$privatize_export = $request->get('privatize_export');
133
134
		$exportOptions = [
135
			'privatize' => $privatize_export,
136
			'toANSI'    => $convert ? 'yes' : 'no',
137
			'path'      => $media_path,
138
		];
139
140
		// What to call the downloaded file
141
		$download_filename = $tree->getName();
142
		if (strtolower(substr($download_filename, -4, 4)) != '.ged') {
143
			$download_filename .= '.ged';
144
		}
145
146
		if ($zip || $media) {
147
			// Export the GEDCOM to an in-memory stream.
148
			$tmp_stream = tmpfile();
149
			FunctionsExport::exportGedcom($tree, $tmp_stream, $exportOptions);
0 ignored issues
show
Bug introduced by
It seems like $tmp_stream can also be of type false; however, parameter $gedout of Fisharebest\Webtrees\Fun...sExport::exportGedcom() 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

149
			FunctionsExport::exportGedcom($tree, /** @scrutinizer ignore-type */ $tmp_stream, $exportOptions);
Loading history...
150
			rewind($tmp_stream);
0 ignored issues
show
Bug introduced by
It seems like $tmp_stream can also be of type false; however, parameter $handle of rewind() 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

150
			rewind(/** @scrutinizer ignore-type */ $tmp_stream);
Loading history...
151
152
			// Create a new/empty .ZIP file
153
			$temp_zip_file  = tempnam(sys_get_temp_dir(), 'webtrees-zip-');
154
			$zip_filesystem = new Filesystem(new ZipArchiveAdapter($temp_zip_file));
155
			$zip_filesystem->writeStream($download_filename, $tmp_stream);
156
157
			if ($media) {
158
				$rows = Database::prepare(
159
					"SELECT m_id, m_gedcom FROM `##media` WHERE m_file = :tree_id"
160
				)->execute([
161
					'tree_id' => $tree->getTreeId(),
162
				])->fetchAll();
163
				$path = $tree->getPreference('MEDIA_DIRECTORY');
164
				foreach ($rows as $row) {
165
					$record = Media::getInstance($row->m_id, $tree, $row->m_gedcom);
166
					if ($record->canShow()) {
167
						foreach ($record->mediaFiles() as $media_file) {
168
							if (file_exists($media_file->getServerFilename())) {
169
								$fp = fopen($media_file->getServerFilename(), 'r');
170
								$zip_filesystem->writeStream($path . $media_file->filename(), $fp);
171
								fclose($fp);
172
							}
173
						}
174
					}
175
				}
176
			}
177
178
			// The ZipArchiveAdapter may or may not close the stream.
179
			if (is_resource($tmp_stream)) {
180
				fclose($tmp_stream);
181
			}
182
183
			// Need to force-close the filesystem
184
			$zip_filesystem = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $zip_filesystem is dead and can be removed.
Loading history...
185
186
			$response = new BinaryFileResponse($temp_zip_file);
187
			$response->deleteFileAfterSend(true);
188
189
			$response->headers->set('Content-Type', 'application/zip');
190
			$response->setContentDisposition(
191
				ResponseHeaderBag::DISPOSITION_ATTACHMENT,
192
				$download_filename . '.zip'
193
			);
194
		} else {
195
			$response = new StreamedResponse(function() use ($tree, $exportOptions) {
196
				$stream = fopen('php://output', 'w');
197
				FunctionsExport::exportGedcom($tree, $stream, $exportOptions);
0 ignored issues
show
Bug introduced by
It seems like $stream can also be of type false; however, parameter $gedout of Fisharebest\Webtrees\Fun...sExport::exportGedcom() 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

197
				FunctionsExport::exportGedcom($tree, /** @scrutinizer ignore-type */ $stream, $exportOptions);
Loading history...
198
				fclose($stream);
0 ignored issues
show
Bug introduced by
It seems like $stream 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

198
				fclose(/** @scrutinizer ignore-type */ $stream);
Loading history...
199
			});
200
201
			$charset = $convert ? 'ISO-8859-1' : 'UTF-8';
202
203
			$response->headers->set('Content-Type', 'text/plain; charset=' . $charset);
204
			$contentDisposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $download_filename);
205
			$response->headers->set('Content-Disposition', $contentDisposition);
206
		}
207
208
		return $response;
209
	}
210
211
	/**
212
	 * @param Request $request
213
	 *
214
	 * @return RedirectResponse
215
	 */
216
	public function exportServer(Request $request): RedirectResponse {
217
		/** @var Tree $tree */
218
		$tree = $request->attributes->get('tree');
219
220
		$filename = WT_DATA_DIR . $tree->getName();
221
222
		// Force a ".ged" suffix
223
		if (strtolower(substr($filename, -4)) != '.ged') {
224
			$filename .= '.ged';
225
		}
226
227
		try {
228
			// To avoid partial trees on timeout/diskspace/etc, write to a temporary file first
229
			$stream = fopen($filename . '.tmp', 'w');
230
			$tree->exportGedcom($stream);
0 ignored issues
show
Bug introduced by
It seems like $stream can also be of type false; however, parameter $stream of Fisharebest\Webtrees\Tree::exportGedcom() 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

230
			$tree->exportGedcom(/** @scrutinizer ignore-type */ $stream);
Loading history...
231
			fclose($stream);
0 ignored issues
show
Bug introduced by
It seems like $stream 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

231
			fclose(/** @scrutinizer ignore-type */ $stream);
Loading history...
232
			rename($filename . '.tmp', $filename);
233
234
			FlashMessages::addMessage(/* I18N: %s is a filename */ I18N::translate('The family tree has been exported to %s.', Html::filename($filename)), 'success');
235
		} catch (Throwable $ex) {
236
			DebugBar::addThrowable($ex);
237
238
			FlashMessages::addMessage(
239
				I18N::translate('The file %s could not be created.', Html::filename($filename)) . '<hr><samp dir="ltr">' . $ex->getMessage() . '</samp>',
240
				'danger'
241
			);
242
		}
243
244
		$url = route('admin-trees', [
245
			'ged' => $tree->getName(),
246
		]);
247
248
		return new RedirectResponse($url);
249
	}
250
251
252
	/**
253
	 * @param Request $request
254
	 *
255
	 * @return RedirectResponse
256
	 */
257
	public function importAction(Request $request): RedirectResponse {
258
		/** @var Tree $tree */
259
		$tree = $request->attributes->get('tree');
260
261
		$source             = $request->get('source');
262
		$keep_media         = (bool) $request->get('keep_media');
263
		$WORD_WRAPPED_NOTES = (bool) $request->get('WORD_WRAPPED_NOTES');
264
		$GEDCOM_MEDIA_PATH  = $request->get('GEDCOM_MEDIA_PATH');
265
266
		// Save these choices as defaults
267
		$tree->setPreference('keep_media', $keep_media ? '1' : '0');
268
		$tree->setPreference('WORD_WRAPPED_NOTES', $WORD_WRAPPED_NOTES ? '1' : '0');
269
		$tree->setPreference('GEDCOM_MEDIA_PATH', $GEDCOM_MEDIA_PATH);
270
271
		if ($source === 'client') {
272
			if (isset($_FILES['tree_name'])) {
273
				if ($_FILES['tree_name']['error'] == 0 && is_readable($_FILES['tree_name']['tmp_name'])) {
274
					$tree->importGedcomFile($_FILES['tree_name']['tmp_name'], $_FILES['tree_name']['name']);
275
				} else {
276
					FlashMessages::addMessage(Functions::fileUploadErrorText($_FILES['tree_name']['error']), 'danger');
277
				}
278
			} else {
279
				FlashMessages::addMessage(I18N::translate('No GEDCOM file was received.'), 'danger');
280
			}
281
		}
282
283
		if ($source === 'server') {
284
			$basename = basename($request->get('tree_name'));
285
286
			if ($basename) {
287
				$tree->importGedcomFile(WT_DATA_DIR . $basename, $basename);
288
			} else {
289
				FlashMessages::addMessage(I18N::translate('No GEDCOM file was received.'), 'danger');
290
			}
291
		}
292
293
		$url = route('admin-trees', ['ged' => $tree->getName()]);
294
295
		return new RedirectResponse($url);
296
	}
297
298
	/**
299
	 * @param Request $request
300
	 *
301
	 * @return Response
302
	 */
303
	public function importForm(Request $request): Response {
304
		/** @var Tree $tree */
305
		$tree = $request->attributes->get('tree');
306
307
		$default_gedcom_file = $tree->getPreference('gedcom_filename');
308
		$gedcom_media_path   = $tree->getPreference('GEDCOM_MEDIA_PATH');
309
		$gedcom_files        = $this->gedcomFiles(WT_DATA_DIR);
310
311
		$title = I18N::translate('Import a GEDCOM file') . ' — ' . e($tree->getTitle());
312
313
		return $this->viewResponse('admin/tree-import', [
314
			'default_gedcom_file' => $default_gedcom_file,
315
			'gedcom_files'        => $gedcom_files,
316
			'gedcom_media_path'   => $gedcom_media_path,
317
			'title'               => $title,
318
		]);
319
	}
320
321
	/**
322
	 * @param Request $request
323
	 *
324
	 * @return Response
325
	 */
326
	public function index(Request $request): Response {
327
		/** @var Tree $tree */
328
		$tree = $request->attributes->get('tree');
329
330
		$multiple_tree_threshold = (int) Site::getPreference('MULTIPLE_TREE_THRESHOLD', self::MULTIPLE_TREE_THRESHOLD);
331
		$gedcom_files            = $this->gedcomFiles(WT_DATA_DIR);
332
333
		$all_trees = Tree::getAll();
334
335
		// On sites with hundreds or thousands of trees, this page becomes very large.
336
		// Just show the current tree, the default tree, and unimported trees
337
		if (count($all_trees) >= $multiple_tree_threshold) {
338
			$all_trees = array_filter($all_trees, function (Tree $x) use ($tree) {
339
				return $x->getPreference('imported') === '0' || $tree->getTreeId() === $x->getTreeId() || $x->getName() === Site::getPreference('DEFAULT_GEDCOM');
340
			});
341
		}
342
343
		$default_tree_name  = $this->generateNewTreeName();
344
		$default_tree_title = I18N::translate('My family tree');
345
346
		$all_users = User::all();
347
348
		$title = I18N::translate('Manage family trees');
349
350
		return $this->viewResponse('admin/trees', [
351
			'all_trees'               => $all_trees,
352
			'all_users'               => $all_users,
353
			'default_tree_name'       => $default_tree_name,
354
			'default_tree_title'      => $default_tree_title,
355
			'gedcom_files'            => $gedcom_files,
356
			'multiple_tree_threshold' => $multiple_tree_threshold,
357
			'title'                   => $title,
358
		]);
359
	}
360
361
	/**
362
	 * @param Request $request
363
	 *
364
	 * @return Response
365
	 */
366
	public function merge(Request $request): Response {
367
		$tree1_name = $request->get('tree1_name');
368
		$tree2_name = $request->get('tree2_name');
369
370
		$tree1 = Tree::findByName($tree1_name);
371
		$tree2 = Tree::findByName($tree2_name);
372
373
		if ($tree1 !== null && $tree2 !== null && $tree1->getTreeId() !== $tree2->getTreeId()) {
374
			$xrefs = $this->commonXrefs($tree1, $tree2);
375
		} else {
376
			$xrefs = [];
377
		}
378
379
		$tree_list = Tree::getNameList();
380
381
		$title = I18N::translate(I18N::translate('Merge family trees'));
382
383
		return $this->viewResponse('admin/trees-merge', [
384
			'tree_list' => $tree_list,
385
			'tree1'     => $tree1,
386
			'tree2'     => $tree2,
387
			'title'     => $title,
388
			'xrefs'     => $xrefs,
389
		]);
390
	}
391
392
	/**
393
	 * @param Request $request
394
	 *
395
	 * @return RedirectResponse
396
	 */
397
	public function mergeAction(Request $request): RedirectResponse {
398
		$tree1_name = $request->get('tree1_name');
399
		$tree2_name = $request->get('tree2_name');
400
401
		$tree1 = Tree::findByName($tree1_name);
402
		$tree2 = Tree::findByName($tree2_name);
403
404
		if ($tree1 !== null && $tree2 !== null && $tree1 !== $tree2 && empty($this->commonXrefs($tree1, $tree2))) {
405
			Database::prepare(
406
				"INSERT INTO `##individuals` (i_id, i_file, i_rin, i_sex, i_gedcom)" .
407
				" SELECT i_id, ?, i_rin, i_sex, i_gedcom FROM `##individuals` AS individuals2 WHERE i_file = ?"
408
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
409
410
			Database::prepare(
411
				"INSERT INTO `##families` (f_id, f_file, f_husb, f_wife, f_gedcom, f_numchil)" .
412
				" SELECT f_id, ?, f_husb, f_wife, f_gedcom, f_numchil FROM `##families` AS families2 WHERE f_file = ?"
413
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
414
415
			Database::prepare(
416
				"INSERT INTO `##sources` (s_id, s_file, s_name, s_gedcom)" .
417
				" SELECT s_id, ?, s_name, s_gedcom FROM `##sources` AS sources2 WHERE s_file = ?"
418
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
419
420
			Database::prepare(
421
				"INSERT INTO `##media` (m_id, m_file, m_gedcom)" .
422
				" SELECT m_id, ?, m_gedcom FROM `##media` AS media2 WHERE m_file = ?"
423
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
424
425
			Database::prepare(
426
				"INSERT INTO `##media_file` (m_id, m_file, multimedia_file_refn, multimedia_format, source_media_type, descriptive_title)" .
427
				" SELECT m_id, ?, multimedia_file_refn, multimedia_format, source_media_type, descriptive_title FROM `##media_file` AS media_file2 WHERE m_file = ?"
428
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
429
430
			Database::prepare(
431
				"INSERT INTO `##other` (o_id, o_file, o_type, o_gedcom)" .
432
				" SELECT o_id, ?, o_type, o_gedcom FROM `##other` AS other2 WHERE o_file = ? AND o_type NOT IN ('HEAD', 'TRLR')"
433
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
434
435
			Database::prepare(
436
				"INSERT INTO `##name` (n_file, n_id, n_num, n_type, n_sort, n_full, n_surname, n_surn, n_givn, n_soundex_givn_std, n_soundex_surn_std, n_soundex_givn_dm, n_soundex_surn_dm)" .
437
				" SELECT ?, n_id, n_num, n_type, n_sort, n_full, n_surname, n_surn, n_givn, n_soundex_givn_std, n_soundex_surn_std, n_soundex_givn_dm, n_soundex_surn_dm FROM `##name` AS name2 WHERE n_file = ?"
438
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
439
440
			Database::prepare(
441
				"INSERT INTO `##placelinks` (pl_p_id, pl_gid, pl_file)" .
442
				" SELECT pl_p_id, pl_gid, ? FROM `##placelinks` AS placelinks2 WHERE pl_file = ?"
443
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
444
445
			Database::prepare(
446
				"INSERT INTO `##dates` (d_day, d_month, d_mon, d_year, d_julianday1, d_julianday2, d_fact, d_gid, d_file, d_type)" .
447
				" SELECT d_day, d_month, d_mon, d_year, d_julianday1, d_julianday2, d_fact, d_gid, ?, d_type FROM `##dates` AS dates2 WHERE d_file = ?"
448
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
449
450
			Database::prepare(
451
				"INSERT INTO `##default_resn` (gedcom_id, xref, tag_type, resn)" .
452
				" SELECT ?, xref, tag_type, resn FROM `##default_resn` AS default_resn2 WHERE gedcom_id = ?"
453
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
454
455
			Database::prepare(
456
				"INSERT INTO `##link` (l_file, l_from, l_type, l_to)" .
457
				" SELECT ?, l_from, l_type, l_to FROM `##link` AS link2 WHERE l_file = ?"
458
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
459
460
			// This table may contain old (deleted) references, which could clash. IGNORE these.
461
			Database::prepare(
462
				"INSERT IGNORE INTO `##change` (change_time, status, gedcom_id, xref, old_gedcom, new_gedcom, user_id)" .
463
				" SELECT change_time, status, ?, xref, old_gedcom, new_gedcom, user_id FROM `##change` AS change2 WHERE gedcom_id = ?"
464
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
465
466
			// This table may contain old (deleted) references, which could clash. IGNORE these.
467
			Database::prepare(
468
				"INSERT IGNORE INTO `##hit_counter` (gedcom_id, page_name, page_parameter, page_count)" .
469
				" SELECT ?, page_name, page_parameter, page_count FROM `##hit_counter` AS hit_counter2 WHERE gedcom_id = ? AND page_name <> 'index.php'"
470
			)->execute([$tree2->getTreeId(), $tree1->getTreeId()]);
471
472
			FlashMessages::addMessage(I18N::translate('The family trees have been merged successfully.'), 'success');
473
474
			$url = route('admin-trees', [
475
				'ged' => $tree2->getName(),
476
			]);
477
		} else {
478
			$url = route('admin-trees-merge', [
479
				'tree1_name' => $tree1->getName(),
480
				'tree2_name' => $tree2->getName(),
481
			]);
482
		}
483
484
		return new RedirectResponse($url);
485
	}
486
487
	/**
488
	 * @param Request $request
489
	 *
490
	 * @return Response
491
	 */
492
	public function places(Request $request): Response {
493
		/** @var Tree $tree */
494
		$tree = $request->attributes->get('tree');
495
496
		$search  = $request->get('search', '');
497
		$replace = $request->get('replace', '');
498
499
		if ($search !== '' && $replace !== '') {
500
			$changes = $this->changePlacesPreview($tree, $search, $replace);
501
		} else {
502
			$changes = [];
503
		}
504
505
		$title = I18N::translate(/* I18N: Renumber the records in a family tree */
506
				'Renumber family tree') . ' — ' . e($tree->getTitle());
507
508
		return $this->viewResponse('admin/trees-places', [
509
			'changes' => $changes,
510
			'replace' => $replace,
511
			'search'  => $search,
512
			'title'   => $title,
513
		]);
514
	}
515
516
	/**
517
	 * @param Request $request
518
	 *
519
	 * @return RedirectResponse
520
	 */
521
	public function placesAction(Request $request): RedirectResponse {
522
		/** @var Tree $tree */
523
		$tree = $request->attributes->get('tree');
524
525
		$search  = $request->get('search', '');
526
		$replace = $request->get('replace', '');
527
528
		$changes = $this->changePlacesUpdate($tree, $search, $replace);
529
530
		$feedback = I18N::translate('The following places have been changed:') . '<ul>';
531
		foreach ($changes as $old_place => $new_place) {
532
			$feedback .= '<li>' . e($old_place) . ' &rarr; ' . e($new_place) . '</li>';
533
		}
534
		$feedback .= '</ul>';
535
536
		FlashMessages::addMessage($feedback, 'success');
537
538
		$url = route('admin-trees-places', [
539
			'ged'     => $tree->getName(),
540
			'replace' => $replace,
541
			'search'  => $search,
542
		]);
543
544
		return new RedirectResponse($url);
545
	}
546
547
	/**
548
	 * @param Request $request
549
	 *
550
	 * @return Response
551
	 */
552
	public function renumber(Request $request): Response {
553
		/** @var Tree $tree */
554
		$tree = $request->attributes->get('tree');
555
556
		$xrefs = $this->duplicateXrefs($tree);
557
558
		$title = I18N::translate(/* I18N: Renumber the records in a family tree */
559
				'Renumber family tree') . ' — ' . e($tree->getTitle());
560
561
		return $this->viewResponse('admin/trees-renumber', [
562
			'title' => $title,
563
			'xrefs' => $xrefs,
564
		]);
565
	}
566
567
	/**
568
	 * @param Request $request
569
	 *
570
	 * @return RedirectResponse
571
	 */
572
	public function renumberAction(Request $request): RedirectResponse {
573
		/** @var Tree $tree */
574
		$tree = $request->attributes->get('tree');
575
576
		$xrefs = $this->duplicateXrefs($tree);
577
578
		foreach ($xrefs as $old_xref => $type) {
579
			$new_xref = $tree->getNewXref();
580
			switch ($type) {
581
				case 'INDI':
582
					Database::prepare(
583
						"UPDATE `##individuals` SET i_id = ?, i_gedcom = REPLACE(i_gedcom, ?, ?) WHERE i_id = ? AND i_file = ?"
584
					)->execute([$new_xref, "0 @$old_xref@ INDI\n", "0 @$new_xref@ INDI\n", $old_xref, $tree->getTreeId()]);
585
					Database::prepare(
586
						"UPDATE `##families` JOIN `##link` ON (l_file = f_file AND l_to = ? AND l_type = 'HUSB') SET f_gedcom = REPLACE(f_gedcom, ?, ?) WHERE f_file = ?"
587
					)->execute([$old_xref, " HUSB @$old_xref@", " HUSB @$new_xref@", $tree->getTreeId()]);
588
					Database::prepare(
589
						"UPDATE `##families` JOIN `##link` ON (l_file = f_file AND l_to = ? AND l_type = 'WIFE') SET f_gedcom = REPLACE(f_gedcom, ?, ?) WHERE f_file = ?"
590
					)->execute([$old_xref, " WIFE @$old_xref@", " WIFE @$new_xref@", $tree->getTreeId()]);
591
					Database::prepare(
592
						"UPDATE `##families` JOIN `##link` ON (l_file = f_file AND l_to = ? AND l_type = 'CHIL') SET f_gedcom = REPLACE(f_gedcom, ?, ?) WHERE f_file = ?"
593
					)->execute([$old_xref, " CHIL @$old_xref@", " CHIL @$new_xref@", $tree->getTreeId()]);
594
					Database::prepare(
595
						"UPDATE `##families` JOIN `##link` ON (l_file = f_file AND l_to = ? AND l_type = 'ASSO') SET f_gedcom = REPLACE(f_gedcom, ?, ?) WHERE f_file = ?"
596
					)->execute([$old_xref, " ASSO @$old_xref@", " ASSO @$new_xref@", $tree->getTreeId()]);
597
					Database::prepare(
598
						"UPDATE `##families` JOIN `##link` ON (l_file = f_file AND l_to = ? AND l_type = '_ASSO') SET f_gedcom = REPLACE(f_gedcom, ?, ?) WHERE f_file = ?"
599
					)->execute([$old_xref, " _ASSO @$old_xref@", " _ASSO @$new_xref@", $tree->getTreeId()]);
600
					Database::prepare(
601
						"UPDATE `##individuals` JOIN `##link` ON (l_file = i_file AND l_to = ? AND l_type = 'ASSO') SET i_gedcom = REPLACE(i_gedcom, ?, ?) WHERE i_file = ?"
602
					)->execute([$old_xref, " ASSO @$old_xref@", " ASSO @$new_xref@", $tree->getTreeId()]);
603
					Database::prepare(
604
						"UPDATE `##individuals` JOIN `##link` ON (l_file = i_file AND l_to = ? AND l_type = '_ASSO') SET i_gedcom = REPLACE(i_gedcom, ?, ?) WHERE i_file = ?"
605
					)->execute([$old_xref, " _ASSO @$old_xref@", " _ASSO @$new_xref@", $tree->getTreeId()]);
606
					Database::prepare(
607
						"UPDATE `##placelinks` SET pl_gid = ? WHERE pl_gid = ? AND pl_file = ?"
608
					)->execute([$new_xref, $old_xref, $tree->getTreeId()]);
609
					Database::prepare(
610
						"UPDATE `##dates` SET d_gid = ? WHERE d_gid = ? AND d_file = ?"
611
					)->execute([$new_xref, $old_xref, $tree->getTreeId()]);
612
					Database::prepare(
613
						"UPDATE `##user_gedcom_setting` SET setting_value = ? WHERE setting_value = ? AND gedcom_id = ? AND setting_name IN ('gedcomid', 'rootid')"
614
					)->execute([$new_xref, $old_xref, $tree->getTreeId()]);
615
					break;
616
				case 'FAM':
617
					Database::prepare(
618
						"UPDATE `##families` SET f_id = ?, f_gedcom = REPLACE(f_gedcom, ?, ?) WHERE f_id = ? AND f_file = ?"
619
					)->execute([$new_xref, "0 @$old_xref@ FAM\n", "0 @$new_xref@ FAM\n", $old_xref, $tree->getTreeId()]);
620
					Database::prepare(
621
						"UPDATE `##individuals` JOIN `##link` ON (l_file = i_file AND l_to = ? AND l_type = 'FAMC') SET i_gedcom = REPLACE(i_gedcom, ?, ?) WHERE i_file = ?"
622
					)->execute([$old_xref, " FAMC @$old_xref@", " FAMC @$new_xref@", $tree->getTreeId()]);
623
					Database::prepare(
624
						"UPDATE `##individuals` JOIN `##link` ON (l_file = i_file AND l_to = ? AND l_type = 'FAMS') SET i_gedcom = REPLACE(i_gedcom, ?, ?) WHERE i_file = ?"
625
					)->execute([$old_xref, " FAMS @$old_xref@", " FAMS @$new_xref@", $tree->getTreeId()]);
626
					Database::prepare(
627
						"UPDATE `##placelinks` SET pl_gid = ? WHERE pl_gid = ? AND pl_file = ?"
628
					)->execute([$new_xref, $old_xref, $tree->getTreeId()]);
629
					Database::prepare(
630
						"UPDATE `##dates` SET d_gid = ? WHERE d_gid = ? AND d_file = ?"
631
					)->execute([$new_xref, $old_xref, $tree->getTreeId()]);
632
					break;
633
				case 'SOUR':
634
					Database::prepare(
635
						"UPDATE `##sources` SET s_id = ?, s_gedcom = REPLACE(s_gedcom, ?, ?) WHERE s_id = ? AND s_file = ?"
636
					)->execute([$new_xref, "0 @$old_xref@ SOUR\n", "0 @$new_xref@ SOUR\n", $old_xref, $tree->getTreeId()]);
637
					Database::prepare(
638
						"UPDATE `##individuals` JOIN `##link` ON (l_file = i_file AND l_to = ? AND l_type = 'SOUR') SET i_gedcom = REPLACE(i_gedcom, ?, ?) WHERE i_file = ?"
639
					)->execute([$old_xref, " SOUR @$old_xref@", " SOUR @$new_xref@", $tree->getTreeId()]);
640
					Database::prepare(
641
						"UPDATE `##families` JOIN `##link` ON (l_file = f_file AND l_to = ? AND l_type = 'SOUR') SET f_gedcom = REPLACE(f_gedcom, ?, ?) WHERE f_file = ?"
642
					)->execute([$old_xref, " SOUR @$old_xref@", " SOUR @$new_xref@", $tree->getTreeId()]);
643
					Database::prepare(
644
						"UPDATE `##media` JOIN `##link` ON (l_file = m_file AND l_to = ? AND l_type = 'SOUR') SET m_gedcom = REPLACE(m_gedcom, ?, ?) WHERE m_file = ?"
645
					)->execute([$old_xref, " SOUR @$old_xref@", " SOUR @$new_xref@", $tree->getTreeId()]);
646
					Database::prepare(
647
						"UPDATE `##other` JOIN `##link` ON (l_file = o_file AND l_to = ? AND l_type = 'SOUR') SET o_gedcom = REPLACE(o_gedcom, ?, ?) WHERE o_file = ?"
648
					)->execute([$old_xref, " SOUR @$old_xref@", " SOUR @$new_xref@", $tree->getTreeId()]);
649
					break;
650
				case 'REPO':
651
					Database::prepare(
652
						"UPDATE `##other` SET o_id = ?, o_gedcom = REPLACE(o_gedcom, ?, ?) WHERE o_id = ? AND o_file = ?"
653
					)->execute([$new_xref, "0 @$old_xref@ REPO\n", "0 @$new_xref@ REPO\n", $old_xref, $tree->getTreeId()]);
654
					Database::prepare(
655
						"UPDATE `##sources` JOIN `##link` ON (l_file = s_file AND l_to = ? AND l_type = 'REPO') SET s_gedcom = REPLACE(s_gedcom, ?, ?) WHERE s_file = ?"
656
					)->execute([$old_xref, " REPO @$old_xref@", " REPO @$new_xref@", $tree->getTreeId()]);
657
					break;
658
				case 'NOTE':
659
					Database::prepare(
660
						"UPDATE `##other` SET o_id = ?, o_gedcom = REPLACE(REPLACE(o_gedcom, ?, ?), ?, ?) WHERE o_id = ? AND o_file = ?"
661
					)->execute([$new_xref, "0 @$old_xref@ NOTE\n", "0 @$new_xref@ NOTE\n", "0 @$old_xref@ NOTE ", "0 @$new_xref@ NOTE ", $old_xref, $tree->getTreeId()]);
662
					Database::prepare(
663
						"UPDATE `##individuals` JOIN `##link` ON (l_file = i_file AND l_to = ? AND l_type = 'NOTE') SET i_gedcom = REPLACE(i_gedcom, ?, ?) WHERE i_file = ?"
664
					)->execute([$old_xref, " NOTE @$old_xref@", " NOTE @$new_xref@", $tree->getTreeId()]);
665
					Database::prepare(
666
						"UPDATE `##families` JOIN `##link` ON (l_file = f_file AND l_to = ? AND l_type = 'NOTE') SET f_gedcom = REPLACE(f_gedcom, ?, ?) WHERE f_file = ?"
667
					)->execute([$old_xref, " NOTE @$old_xref@", " NOTE @$new_xref@", $tree->getTreeId()]);
668
					Database::prepare(
669
						"UPDATE `##media` JOIN `##link` ON (l_file = m_file AND l_to = ? AND l_type = 'NOTE') SET m_gedcom = REPLACE(m_gedcom, ?, ?) WHERE m_file = ?"
670
					)->execute([$old_xref, " NOTE @$old_xref@", " NOTE @$new_xref@", $tree->getTreeId()]);
671
					Database::prepare(
672
						"UPDATE `##sources` JOIN `##link` ON (l_file = s_file AND l_to = ? AND l_type = 'NOTE') SET s_gedcom = REPLACE(s_gedcom, ?, ?) WHERE s_file = ?"
673
					)->execute([$old_xref, " NOTE @$old_xref@", " NOTE @$new_xref@", $tree->getTreeId()]);
674
					Database::prepare(
675
						"UPDATE `##other` JOIN `##link` ON (l_file = o_file AND l_to = ? AND l_type = 'NOTE') SET o_gedcom = REPLACE(o_gedcom, ?, ?) WHERE o_file = ?"
676
					)->execute([$old_xref, " NOTE @$old_xref@", " NOTE @$new_xref@", $tree->getTreeId()]);
677
					break;
678
				case 'OBJE':
679
					Database::prepare(
680
						"UPDATE `##media` SET m_id = ?, m_gedcom = REPLACE(m_gedcom, ?, ?) WHERE m_id = ? AND m_file = ?"
681
					)->execute([$new_xref, "0 @$old_xref@ OBJE\n", "0 @$new_xref@ OBJE\n", $old_xref, $tree->getTreeId()]);
682
					Database::prepare(
683
						"UPDATE `##media_file` SET m_id = ? WHERE m_id = ? AND m_file = ?"
684
					)->execute([$new_xref, $old_xref, $tree->getTreeId()]);
685
					Database::prepare(
686
						"UPDATE `##individuals` JOIN `##link` ON (l_file = i_file AND l_to = ? AND l_type = 'OBJE') SET i_gedcom = REPLACE(i_gedcom, ?, ?) WHERE i_file = ?"
687
					)->execute([$old_xref, " OBJE @$old_xref@", " OBJE @$new_xref@", $tree->getTreeId()]);
688
					Database::prepare(
689
						"UPDATE `##families` JOIN `##link` ON (l_file = f_file AND l_to = ? AND l_type = 'OBJE') SET f_gedcom = REPLACE(f_gedcom, ?, ?) WHERE f_file = ?"
690
					)->execute([$old_xref, " OBJE @$old_xref@", " OBJE @$new_xref@", $tree->getTreeId()]);
691
					Database::prepare(
692
						"UPDATE `##media` JOIN `##link` ON (l_file = m_file AND l_to = ? AND l_type = 'OBJE') SET m_gedcom = REPLACE(m_gedcom, ?, ?) WHERE m_file = ?"
693
					)->execute([$old_xref, " OBJE @$old_xref@", " OBJE @$new_xref@", $tree->getTreeId()]);
694
					Database::prepare(
695
						"UPDATE `##sources` JOIN `##link` ON (l_file = s_file AND l_to = ? AND l_type = 'OBJE') SET s_gedcom = REPLACE(s_gedcom, ?, ?) WHERE s_file = ?"
696
					)->execute([$old_xref, " OBJE @$old_xref@", " OBJE @$new_xref@", $tree->getTreeId()]);
697
					Database::prepare(
698
						"UPDATE `##other` JOIN `##link` ON (l_file = o_file AND l_to = ? AND l_type = 'OBJE') SET o_gedcom = REPLACE(o_gedcom, ?, ?) WHERE o_file = ?"
699
					)->execute([$old_xref, " OBJE @$old_xref@", " OBJE @$new_xref@", $tree->getTreeId()]);
700
					break;
701
				default:
702
					Database::prepare(
703
						"UPDATE `##other` SET o_id = ?, o_gedcom = REPLACE(o_gedcom, ?, ?) WHERE o_id = ? AND o_file = ?"
704
					)->execute([$new_xref, "0 @$old_xref@ $type\n", "0 @$new_xref@ $type\n", $old_xref, $tree->getTreeId()]);
705
					Database::prepare(
706
						"UPDATE `##individuals` JOIN `##link` ON (l_file = i_file AND l_to = ?) SET i_gedcom = REPLACE(i_gedcom, ?, ?) WHERE i_file = ?"
707
					)->execute([$old_xref, " @$old_xref@", " @$new_xref@", $tree->getTreeId()]);
708
					Database::prepare(
709
						"UPDATE `##families` JOIN `##link` ON (l_file = f_file AND l_to = ?) SET f_gedcom = REPLACE(f_gedcom, ?, ?) WHERE f_file = ?"
710
					)->execute([$old_xref, " @$old_xref@", " @$new_xref@", $tree->getTreeId()]);
711
					Database::prepare(
712
						"UPDATE `##media` JOIN `##link` ON (l_file = m_file AND l_to = ?) SET m_gedcom = REPLACE(m_gedcom, ?, ?) WHERE m_file = ?"
713
					)->execute([$old_xref, " @$old_xref@", " @$new_xref@", $tree->getTreeId()]);
714
					Database::prepare(
715
						"UPDATE `##sources` JOIN `##link` ON (l_file = s_file AND l_to = ?) SET s_gedcom = REPLACE(s_gedcom, ?, ?) WHERE s_file = ?"
716
					)->execute([$old_xref, " @$old_xref@", " @$new_xref@", $tree->getTreeId()]);
717
					Database::prepare(
718
						"UPDATE `##other` JOIN `##link` ON (l_file = o_file AND l_to = ?) SET o_gedcom = REPLACE(o_gedcom, ?, ?) WHERE o_file = ?"
719
					)->execute([$old_xref, " @$old_xref@", " @$new_xref@", $tree->getTreeId()]);
720
					break;
721
			}
722
			Database::prepare(
723
				"UPDATE `##name` SET n_id = ? WHERE n_id = ? AND n_file = ?"
724
			)->execute([$new_xref, $old_xref, $tree->getTreeId()]);
725
			Database::prepare(
726
				"UPDATE `##default_resn` SET xref = ? WHERE xref = ? AND gedcom_id = ?"
727
			)->execute([$new_xref, $old_xref, $tree->getTreeId()]);
728
			Database::prepare(
729
				"UPDATE `##hit_counter` SET page_parameter = ? WHERE page_parameter = ? AND gedcom_id = ?"
730
			)->execute([$new_xref, $old_xref, $tree->getTreeId()]);
731
			Database::prepare(
732
				"UPDATE `##link` SET l_from = ? WHERE l_from = ? AND l_file = ?"
733
			)->execute([$new_xref, $old_xref, $tree->getTreeId()]);
734
			Database::prepare(
735
				"UPDATE `##link` SET l_to = ? WHERE l_to = ? AND l_file = ?"
736
			)->execute([$new_xref, $old_xref, $tree->getTreeId()]);
737
738
			unset($xrefs[$old_xref]);
739
740
			try {
741
				Database::prepare(
742
					"UPDATE `##favorite` SET xref = ? WHERE xref = ? AND gedcom_id = ?"
743
				)->execute([$new_xref, $old_xref, $tree->getTreeId()]);
744
			} catch (\Exception $ex) {
745
				DebugBar::addThrowable($ex);
746
747
				// Perhaps the favorites module was not installed?
748
			}
749
750
			// How much time do we have left?
751
			if (microtime(true) - WT_START_TIME > ini_get('max_execution_time') - 5) {
752
				FlashMessages::addMessage(I18N::translate('The server’s time limit has been reached.'), 'warning');
753
				break;
754
			}
755
		}
756
757
		$url = route('admin-trees-renumber', ['ged' => $tree->getName()]);
758
759
		return new RedirectResponse($url);
760
	}
761
762
	/**
763
	 * @param Request $request
764
	 *
765
	 * @return RedirectResponse
766
	 */
767
	public function setDefault(Request $request): RedirectResponse {
768
		/** @var Tree $tree */
769
		$tree = $request->attributes->get('tree');
770
771
		Site::setPreference('DEFAULT_GEDCOM', $tree->getName());
772
773
		FlashMessages::addMessage(/* I18N: %s is the name of a family tree */
774
			I18N::translate('The family tree “%s” will be shown to visitors when they first arrive at this website.', e($tree->getTitle())), 'success');
775
776
		$url = route('admin-trees');
777
778
		return new RedirectResponse($url);
779
	}
780
781
	/**
782
	 * @param Request $request
783
	 *
784
	 * @return RedirectResponse
785
	 */
786
	public function synchronize(Request $request): RedirectResponse {
787
		/** @var Tree $tree */
788
		$tree = $request->attributes->get('tree');
789
790
		$gedcom_files = $this->gedcomFiles(WT_DATA_DIR);
791
792
		foreach ($gedcom_files as $gedcom_file) {
793
			// Only import files that have changed
794
			$filemtime = (string) filemtime(WT_DATA_DIR . $gedcom_file);
795
796
			$tree = Tree::findByName($gedcom_file) ?? Tree::create($gedcom_file, $gedcom_file);
797
798
			if ($tree->getPreference('filemtime') !== $filemtime) {
799
				$tree->importGedcomFile(WT_DATA_DIR . $gedcom_file, $gedcom_file);
800
				$tree->setPreference('filemtime', $filemtime);
801
802
				FlashMessages::addMessage(I18N::translate('The GEDCOM file “%s” has been imported.', e($gedcom_file)), 'success');
803
			}
804
		}
805
806
		foreach (Tree::getAll() as $tree) {
807
			if (!in_array($tree->getName(), $gedcom_files)) {
808
				FlashMessages::addMessage(I18N::translate('The family tree “%s” has been deleted.', e($tree->getTitle())), 'success');
809
				$tree->delete();
810
			}
811
		}
812
813
		$url = route('admin-trees', ['ged' => $tree->getName()]);
814
815
		return new RedirectResponse($url);
816
	}
817
818
	/**
819
	 * @param Request $request
820
	 *
821
	 * @return Response
822
	 */
823
	public function unconnected(Request $request): Response {
824
		/** @var Tree $tree */
825
		$tree = $request->attributes->get('tree');
826
827
		/** @var User $user */
828
		$user = $request->attributes->get('user');
829
830
		$associates = (bool) $request->get('associates');
831
832
		if ($associates) {
833
			$sql = "SELECT l_from, l_to FROM `##link` WHERE l_file = :tree_id AND l_type IN ('FAMS', 'FAMC', 'ASSO', '_ASSO')";
834
		} else {
835
			$sql = "SELECT l_from, l_to FROM `##link` WHERE l_file = :tree_id AND l_type IN ('FAMS', 'FAMC')";
836
		}
837
838
		$rows  = Database::prepare($sql)->execute([
839
			'tree_id' => $tree->getTreeId(),
840
		])->fetchAll();
841
		$graph = [];
842
843
		foreach ($rows as $row) {
844
			$graph[$row->l_from][$row->l_to] = 1;
845
			$graph[$row->l_to][$row->l_from] = 1;
846
		}
847
848
		$algorithm  = new ConnectedComponent($graph);
849
		$components = $algorithm->findConnectedComponents();
850
		$root       = $tree->significantIndividual($user);
851
		$xref       = $root->getXref();
852
853
		/** @var Individual[][] */
854
		$individual_groups = [];
855
856
		foreach ($components as $component) {
857
			if (!in_array($xref, $component)) {
858
				$individuals = [];
859
				foreach ($component as $xref) {
860
					$individuals[] = Individual::getInstance($xref, $tree);
861
				}
862
				// The database query may return pending additions/deletions, which may not exist.
863
				$individual_groups[] = array_filter($individuals);
864
			}
865
		}
866
867
		$title = I18N::translate('Find unrelated individuals') . ' — ' . e($tree->getTitle());
868
869
		return $this->viewResponse('admin/trees-unconnected', [
870
			'associates'        => $associates,
871
			'root'              => $root,
872
			'individual_groups' => $individual_groups,
873
			'title'             => $title,
874
		]);
875
	}
876
877
	/**
878
	 * Find a list of place names that would be updated.
879
	 *
880
	 * @param Tree   $tree
881
	 * @param string $search
882
	 * @param string $replace
883
	 *
884
	 * @return string[]
885
	 */
886
	private function changePlacesPreview(Tree $tree, string $search, string $replace): array {
887
		$changes = [];
888
889
		$rows = Database::prepare(
890
			"SELECT i_id AS xref, COALESCE(new_gedcom, i_gedcom) AS gedcom" .
891
			" FROM `##individuals`" .
892
			" LEFT JOIN `##change` ON (i_id = xref AND i_file=gedcom_id AND status='pending')" .
893
			" WHERE i_file = ?" .
894
			" AND COALESCE(new_gedcom, i_gedcom) REGEXP CONCAT('\n2 PLAC ([^\n]*, )*', ?, '(\n|$)')"
895
		)->execute([$tree->getTreeId(), preg_quote($search)])->fetchAll();
896
		foreach ($rows as $row) {
897
			$record = Individual::getInstance($row->xref, $tree, $row->gedcom);
898
			foreach ($record->getFacts() as $fact) {
899
				$old_place = $fact->getAttribute('PLAC');
900
				if (preg_match('/(^|, )' . preg_quote($search, '/') . '$/i', $old_place)) {
901
					$new_place           = preg_replace('/(^|, )' . preg_quote($search, '/') . '$/i', '$1' . $replace, $old_place);
902
					$changes[$old_place] = $new_place;
903
				}
904
			}
905
		}
906
		$rows = Database::prepare(
907
			"SELECT f_id AS xref, COALESCE(new_gedcom, f_gedcom) AS gedcom" .
908
			" FROM `##families`" .
909
			" LEFT JOIN `##change` ON (f_id = xref AND f_file=gedcom_id AND status='pending')" .
910
			" WHERE f_file = ?" .
911
			" AND COALESCE(new_gedcom, f_gedcom) REGEXP CONCAT('\n2 PLAC ([^\n]*, )*', ?, '(\n|$)')"
912
		)->execute([$tree->getTreeId(), preg_quote($search)])->fetchAll();
913
		foreach ($rows as $row) {
914
			$record = Family::getInstance($row->xref, $tree, $row->gedcom);
915
			foreach ($record->getFacts() as $fact) {
916
				$old_place = $fact->getAttribute('PLAC');
917
				if (preg_match('/(^|, )' . preg_quote($search, '/') . '$/i', $old_place)) {
918
					$new_place           = preg_replace('/(^|, )' . preg_quote($search, '/') . '$/i', '$1' . $replace, $old_place);
919
					$changes[$old_place] = $new_place;
920
				}
921
			}
922
		}
923
924
		asort($changes);
925
926
		return $changes;
927
	}
928
929
	/**
930
	 * Find a list of place names that would be updated.
931
	 *
932
	 * @param Tree   $tree
933
	 * @param string $search
934
	 * @param string $replace
935
	 *
936
	 * @return string[]
937
	 */
938
	private function changePlacesUpdate(Tree $tree, string $search, string $replace): array {
939
		$changes = [];
940
941
		$rows = Database::prepare(
942
			"SELECT i_id AS xref, COALESCE(new_gedcom, i_gedcom) AS gedcom" .
943
			" FROM `##individuals`" .
944
			" LEFT JOIN `##change` ON (i_id = xref AND i_file=gedcom_id AND status='pending')" .
945
			" WHERE i_file = ?" .
946
			" AND COALESCE(new_gedcom, i_gedcom) REGEXP CONCAT('\n2 PLAC ([^\n]*, )*', ?, '(\n|$)')"
947
		)->execute([$tree->getTreeId(), preg_quote($search)])->fetchAll();
948
		foreach ($rows as $row) {
949
			$record = Individual::getInstance($row->xref, $tree, $row->gedcom);
950
			foreach ($record->getFacts() as $fact) {
951
				$old_place = $fact->getAttribute('PLAC');
952
				if (preg_match('/(^|, )' . preg_quote($search, '/') . '$/i', $old_place)) {
953
					$new_place           = preg_replace('/(^|, )' . preg_quote($search, '/') . '$/i', '$1' . $replace, $old_place);
954
					$changes[$old_place] = $new_place;
955
					$gedcom              = preg_replace('/(\n2 PLAC (?:.*, )*)' . preg_quote($search, '/') . '(\n|$)/i', '$1' . $replace . '$2', $fact->getGedcom());
956
					$record->updateFact($fact->getFactId(), $gedcom, false);
957
				}
958
			}
959
		}
960
		$rows = Database::prepare(
961
			"SELECT f_id AS xref, COALESCE(new_gedcom, f_gedcom) AS gedcom" .
962
			" FROM `##families`" .
963
			" LEFT JOIN `##change` ON (f_id = xref AND f_file=gedcom_id AND status='pending')" .
964
			" WHERE f_file = ?" .
965
			" AND COALESCE(new_gedcom, f_gedcom) REGEXP CONCAT('\n2 PLAC ([^\n]*, )*', ?, '(\n|$)')"
966
		)->execute([$tree->getTreeId(), preg_quote($search)])->fetchAll();
967
		foreach ($rows as $row) {
968
			$record = Family::getInstance($row->xref, $tree, $row->gedcom);
969
			foreach ($record->getFacts() as $fact) {
970
				$old_place = $fact->getAttribute('PLAC');
971
				if (preg_match('/(^|, )' . preg_quote($search, '/') . '$/i', $old_place)) {
972
					$new_place           = preg_replace('/(^|, )' . preg_quote($search, '/') . '$/i', '$1' . $replace, $old_place);
973
					$changes[$old_place] = $new_place;
974
					$gedcom              = preg_replace('/(\n2 PLAC (?:.*, )*)' . preg_quote($search, '/') . '(\n|$)/i', '$1' . $replace . '$2', $fact->getGedcom());
975
					$record->updateFact($fact->getFactId(), $gedcom, false);
976
				}
977
			}
978
		}
979
980
		asort($changes);
981
982
		return $changes;
983
	}
984
985
	/**
986
	 * Every XREF used by two trees at the same time.
987
	 *
988
	 * @param Tree $tree
989
	 *
990
	 * @return string[]
991
	 */
992
	private function commonXrefs(Tree $tree1, Tree $tree2): array {
993
		return Database::prepare(
994
			"SELECT xref, type FROM (" .
995
			" SELECT i_id AS xref, 'INDI' AS type FROM `##individuals` WHERE i_file = ?" .
996
			"  UNION " .
997
			" SELECT f_id AS xref, 'FAM' AS type FROM `##families` WHERE f_file = ?" .
998
			"  UNION " .
999
			" SELECT s_id AS xref, 'SOUR' AS type FROM `##sources` WHERE s_file = ?" .
1000
			"  UNION " .
1001
			" SELECT m_id AS xref, 'OBJE' AS type FROM `##media` WHERE m_file = ?" .
1002
			"  UNION " .
1003
			" SELECT o_id AS xref, o_type AS type FROM `##other` WHERE o_file = ? AND o_type NOT IN ('HEAD', 'TRLR')" .
1004
			") AS this_tree JOIN (" .
1005
			" SELECT xref FROM `##change` WHERE gedcom_id = ?" .
1006
			"  UNION " .
1007
			" SELECT i_id AS xref FROM `##individuals` WHERE i_file = ?" .
1008
			"  UNION " .
1009
			" SELECT f_id AS xref FROM `##families` WHERE f_file = ?" .
1010
			"  UNION " .
1011
			" SELECT s_id AS xref FROM `##sources` WHERE s_file = ?" .
1012
			"  UNION " .
1013
			" SELECT m_id AS xref FROM `##media` WHERE m_file = ?" .
1014
			"  UNION " .
1015
			" SELECT o_id AS xref FROM `##other` WHERE o_file = ? AND o_type NOT IN ('HEAD', 'TRLR')" .
1016
			") AS other_trees USING (xref)"
1017
		)->execute([
1018
			$tree1->getTreeId(),
1019
			$tree1->getTreeId(),
1020
			$tree1->getTreeId(),
1021
			$tree1->getTreeId(),
1022
			$tree1->getTreeId(),
1023
			$tree2->getTreeId(),
1024
			$tree2->getTreeId(),
1025
			$tree2->getTreeId(),
1026
			$tree2->getTreeId(),
1027
			$tree2->getTreeId(),
1028
			$tree2->getTreeId(),
1029
		])->fetchAssoc();
1030
	}
1031
1032
	/**
1033
	 * Every XREF used by this tree and also used by some other tree
1034
	 *
1035
	 * @param Tree $tree
1036
	 *
1037
	 * @return string[]
1038
	 */
1039
	private function duplicateXrefs(Tree $tree): array {
1040
		return Database::prepare(
1041
			"SELECT xref, type FROM (" .
1042
			" SELECT i_id AS xref, 'INDI' AS type FROM `##individuals` WHERE i_file = :tree_id_1" .
1043
			"  UNION " .
1044
			" SELECT f_id AS xref, 'FAM' AS type FROM `##families` WHERE f_file = :tree_id_2" .
1045
			"  UNION " .
1046
			" SELECT s_id AS xref, 'SOUR' AS type FROM `##sources` WHERE s_file = :tree_id_3" .
1047
			"  UNION " .
1048
			" SELECT m_id AS xref, 'OBJE' AS type FROM `##media` WHERE m_file = :tree_id_4" .
1049
			"  UNION " .
1050
			" SELECT o_id AS xref, o_type AS type FROM `##other` WHERE o_file = :tree_id_5 AND o_type NOT IN ('HEAD', 'TRLR')" .
1051
			") AS this_tree JOIN (" .
1052
			" SELECT xref FROM `##change` WHERE gedcom_id <> :tree_id_6" .
1053
			"  UNION " .
1054
			" SELECT i_id AS xref FROM `##individuals` WHERE i_file <> :tree_id_7" .
1055
			"  UNION " .
1056
			" SELECT f_id AS xref FROM `##families` WHERE f_file <> :tree_id_8" .
1057
			"  UNION " .
1058
			" SELECT s_id AS xref FROM `##sources` WHERE s_file <> :tree_id_9" .
1059
			"  UNION " .
1060
			" SELECT m_id AS xref FROM `##media` WHERE m_file <> :tree_id_10" .
1061
			"  UNION " .
1062
			" SELECT o_id AS xref FROM `##other` WHERE o_file <> :tree_id_11 AND o_type NOT IN ('HEAD', 'TRLR')" .
1063
			") AS other_trees USING (xref)"
1064
		)->execute([
1065
			'tree_id_1'  => $tree->getTreeId(),
1066
			'tree_id_2'  => $tree->getTreeId(),
1067
			'tree_id_3'  => $tree->getTreeId(),
1068
			'tree_id_4'  => $tree->getTreeId(),
1069
			'tree_id_5'  => $tree->getTreeId(),
1070
			'tree_id_6'  => $tree->getTreeId(),
1071
			'tree_id_7'  => $tree->getTreeId(),
1072
			'tree_id_8'  => $tree->getTreeId(),
1073
			'tree_id_9'  => $tree->getTreeId(),
1074
			'tree_id_10' => $tree->getTreeId(),
1075
			'tree_id_11' => $tree->getTreeId(),
1076
		])->fetchAssoc();
1077
	}
1078
1079
	/**
1080
	 * Find a list of GEDCOM files in a folder
1081
	 *
1082
	 * @param string $folder
1083
	 *
1084
	 * @return array
1085
	 */
1086
	private function gedcomFiles(string $folder): array {
1087
		$d     = opendir($folder);
1088
		$files = [];
1089
		while (($f = readdir($d)) !== false) {
0 ignored issues
show
Bug introduced by
It seems like $d can also be of type false; however, parameter $dir_handle of readdir() 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

1089
		while (($f = readdir(/** @scrutinizer ignore-type */ $d)) !== false) {
Loading history...
1090
			if (!is_dir(WT_DATA_DIR . $f) && is_readable(WT_DATA_DIR . $f)) {
1091
				$fp     = fopen(WT_DATA_DIR . $f, 'rb');
1092
				$header = fread($fp, 64);
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

1092
				$header = fread(/** @scrutinizer ignore-type */ $fp, 64);
Loading history...
1093
				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

1093
				fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
1094
				if (preg_match('/^(' . WT_UTF8_BOM . ')?0 *HEAD/', $header)) {
1095
					$files[] = $f;
1096
				}
1097
			}
1098
		}
1099
		sort($files);
1100
1101
		return $files;
1102
	}
1103
1104
	/**
1105
	 * Generate a unqiue name for new trees
1106
	 *
1107
	 * @return string
1108
	 */
1109
	private function generateNewTreeName(): string {
1110
		$tree_name      = 'tree';
1111
		$tree_number    = 1;
1112
		$existing_trees = Tree::getNameList();
1113
1114
		while (array_key_exists($tree_name . $tree_number, $existing_trees)) {
1115
			$tree_number++;
1116
		}
1117
1118
		return $tree_name . $tree_number;
1119
	}
1120
}
1121