Completed
Push — master ( 56eca4...2839b8 )
by Greg
06:20
created

AdminTreesController::duplicateRecords()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 73
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 59
nc 1
nop 1
dl 0
loc 73
rs 8.8945
c 1
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\Controllers;
21
22
use Exception;
23
use Fisharebest\Webtrees\Auth;
24
use Fisharebest\Webtrees\Contracts\UserInterface;
25
use Fisharebest\Webtrees\Date;
26
use Fisharebest\Webtrees\Family;
27
use Fisharebest\Webtrees\FlashMessages;
28
use Fisharebest\Webtrees\Functions\Functions;
29
use Fisharebest\Webtrees\Gedcom;
30
use Fisharebest\Webtrees\GedcomRecord;
31
use Fisharebest\Webtrees\GedcomTag;
32
use Fisharebest\Webtrees\I18N;
33
use Fisharebest\Webtrees\Individual;
34
use Fisharebest\Webtrees\Media;
35
use Fisharebest\Webtrees\Module\ModuleThemeInterface;
36
use Fisharebest\Webtrees\Services\ModuleService;
37
use Fisharebest\Webtrees\Services\TimeoutService;
38
use Fisharebest\Webtrees\Services\TreeService;
39
use Fisharebest\Webtrees\Services\UserService;
40
use Fisharebest\Webtrees\Site;
41
use Fisharebest\Webtrees\Source;
42
use Fisharebest\Webtrees\SurnameTradition;
43
use Fisharebest\Webtrees\Tree;
44
use Illuminate\Database\Capsule\Manager as DB;
45
use Illuminate\Database\Query\Builder;
46
use Illuminate\Database\Query\Expression;
47
use Illuminate\Database\Query\JoinClause;
48
use Illuminate\Support\Collection;
49
use League\Flysystem\FilesystemInterface;
50
use Nyholm\Psr7\UploadedFile;
51
use Psr\Http\Message\ResponseInterface;
52
use Psr\Http\Message\ServerRequestInterface;
53
use Psr\Http\Message\StreamFactoryInterface;
54
use stdClass;
55
56
use function app;
57
use function array_key_exists;
58
use function assert;
59
use function fclose;
60
use function preg_match;
61
use function route;
62
63
use const UPLOAD_ERR_OK;
64
65
/**
66
 * Controller for tree administration.
67
 */
68
class AdminTreesController extends AbstractBaseController
69
{
70
    // Show a reduced page when there are more than a certain number of trees
71
    private const MULTIPLE_TREE_THRESHOLD = '500';
72
73
    /** @var string */
74
    protected $layout = 'layouts/administration';
75
76
    /** @var ModuleService */
77
    private $module_service;
78
79
    /** @var FilesystemInterface */
80
    private $filesystem;
81
82
    /** @var TimeoutService */
83
    private $timeout_service;
84
85
    /** @var TreeService */
86
    private $tree_service;
87
88
    /** @var UserService */
89
    private $user_service;
90
91
    /**
92
     * AdminTreesController constructor.
93
     *
94
     * @param FilesystemInterface $filesystem
95
     * @param ModuleService       $module_service
96
     * @param TimeoutService      $timeout_service
97
     * @param TreeService         $tree_service
98
     * @param UserService         $user_service
99
     */
100
    public function __construct(
101
        FilesystemInterface $filesystem,
102
        ModuleService $module_service,
103
        TimeoutService $timeout_service,
104
        TreeService $tree_service,
105
        UserService $user_service
106
    ) {
107
        $this->filesystem      = $filesystem;
108
        $this->module_service  = $module_service;
109
        $this->timeout_service = $timeout_service;
110
        $this->tree_service    = $tree_service;
111
        $this->user_service    = $user_service;
112
    }
113
114
    /**
115
     * @param ServerRequestInterface $request
116
     *
117
     * @return ResponseInterface
118
     */
119
    public function check(ServerRequestInterface $request): ResponseInterface
120
    {
121
        $tree = $request->getAttribute('tree');
122
        assert($tree instanceof Tree);
123
124
        // We need to work with raw GEDCOM data, as we are looking for errors
125
        // which may prevent the GedcomRecord objects from working.
126
127
        $q1 = DB::table('individuals')
128
            ->where('i_file', '=', $tree->id())
129
            ->select(['i_id AS xref', 'i_gedcom AS gedcom', new Expression("'INDI' AS type")]);
130
        $q2 = DB::table('families')
131
            ->where('f_file', '=', $tree->id())
132
            ->select(['f_id AS xref', 'f_gedcom AS gedcom', new Expression("'FAM' AS type")]);
133
        $q3 = DB::table('media')
134
            ->where('m_file', '=', $tree->id())
135
            ->select(['m_id AS xref', 'm_gedcom AS gedcom', new Expression("'OBJE' AS type")]);
136
        $q4 = DB::table('sources')
137
            ->where('s_file', '=', $tree->id())
138
            ->select(['s_id AS xref', 's_gedcom AS gedcom', new Expression("'SOUR' AS type")]);
139
        $q5 = DB::table('other')
140
            ->where('o_file', '=', $tree->id())
141
            ->whereNotIn('o_type', ['HEAD', 'TRLR'])
142
            ->select(['o_id AS xref', 'o_gedcom AS gedcom', 'o_type']);
143
        $q6 = DB::table('change')
144
            ->where('gedcom_id', '=', $tree->id())
145
            ->where('status', '=', 'pending')
146
            ->orderBy('change_id')
147
            ->select(['xref', 'new_gedcom AS gedcom', new Expression("'' AS type")]);
148
149
        $rows = $q1
150
            ->unionAll($q2)
151
            ->unionAll($q3)
152
            ->unionAll($q4)
153
            ->unionAll($q5)
154
            ->unionAll($q6)
155
            ->get()
156
            ->map(static function (stdClass $row): stdClass {
157
                // Extract type for pending record
158
                if ($row->type === '' && preg_match('/^0 @[^@]*@ ([_A-Z0-9]+)/', $row->gedcom, $match)) {
159
                    $row->type = $match[1];
160
                }
161
162
                return $row;
163
            });
164
165
        $records = [];
166
167
        foreach ($rows as $row) {
168
            if ($row->gedcom !== '') {
169
                // existing or updated record
170
                $records[$row->xref] = $row;
171
            } else {
172
                // deleted record
173
                unset($records[$row->xref]);
174
            }
175
        }
176
177
        // LOOK FOR BROKEN LINKS
178
        $XREF_LINKS = [
179
            'NOTE'          => 'NOTE',
180
            'SOUR'          => 'SOUR',
181
            'REPO'          => 'REPO',
182
            'OBJE'          => 'OBJE',
183
            'SUBM'          => 'SUBM',
184
            'FAMC'          => 'FAM',
185
            'FAMS'          => 'FAM',
186
            //'ADOP'=>'FAM', // Need to handle this case specially. We may have both ADOP and FAMC links to the same FAM, but only store one.
187
            'HUSB'          => 'INDI',
188
            'WIFE'          => 'INDI',
189
            'CHIL'          => 'INDI',
190
            'ASSO'          => 'INDI',
191
            '_ASSO'         => 'INDI',
192
            // A webtrees extension
193
            'ALIA'          => 'INDI',
194
            'AUTH'          => 'INDI',
195
            // A webtrees extension
196
            'ANCI'          => 'SUBM',
197
            'DESI'          => 'SUBM',
198
            '_WT_OBJE_SORT' => 'OBJE',
199
            '_LOC'          => '_LOC',
200
        ];
201
202
        $RECORD_LINKS = [
203
            'INDI' => [
204
                'NOTE',
205
                'OBJE',
206
                'SOUR',
207
                'SUBM',
208
                'ASSO',
209
                '_ASSO',
210
                'FAMC',
211
                'FAMS',
212
                'ALIA',
213
                '_WT_OBJE_SORT',
214
                '_LOC',
215
            ],
216
            'FAM'  => [
217
                'NOTE',
218
                'OBJE',
219
                'SOUR',
220
                'SUBM',
221
                'ASSO',
222
                '_ASSO',
223
                'HUSB',
224
                'WIFE',
225
                'CHIL',
226
                '_LOC',
227
            ],
228
            'SOUR' => [
229
                'NOTE',
230
                'OBJE',
231
                'REPO',
232
                'AUTH',
233
            ],
234
            'REPO' => ['NOTE'],
235
            'OBJE' => ['NOTE'],
236
            // The spec also allows SOUR, but we treat this as a warning
237
            'NOTE' => [],
238
            // The spec also allows SOUR, but we treat this as a warning
239
            'SUBM' => [
240
                'NOTE',
241
                'OBJE',
242
            ],
243
            'SUBN' => ['SUBM'],
244
            '_LOC' => [
245
                'SOUR',
246
                'OBJE',
247
                '_LOC',
248
                'NOTE',
249
            ],
250
        ];
251
252
        $errors   = [];
253
        $warnings = [];
254
255
        // Generate lists of all links
256
        $all_links   = [];
257
        $upper_links = [];
258
        foreach ($records as $record) {
259
            $all_links[$record->xref]               = [];
260
            $upper_links[strtoupper($record->xref)] = $record->xref;
261
            preg_match_all('/\n\d (' . Gedcom::REGEX_TAG . ') @([^#@\n][^\n@]*)@/', $record->gedcom, $matches, PREG_SET_ORDER);
262
            foreach ($matches as $match) {
263
                $all_links[$record->xref][$match[2]] = $match[1];
264
            }
265
        }
266
267
        foreach ($all_links as $xref1 => $links) {
268
            $type1 = $records[$xref1]->type;
269
            foreach ($links as $xref2 => $type2) {
270
                $type3 = isset($records[$xref2]) ? $records[$xref2]->type : '';
271
                if (!array_key_exists($xref2, $all_links)) {
272
                    if (array_key_exists(strtoupper($xref2), $upper_links)) {
273
                        $warnings[] =
274
                            $this->checkLinkMessage($tree, $type1, $xref1, $type2, $xref2) . ' ' .
275
                            /* I18N: placeholders are GEDCOM XREFs, such as R123 */
276
                            I18N::translate('%1$s does not exist. Did you mean %2$s?', $this->checkLink($tree, $xref2), $this->checkLink($tree, $upper_links[strtoupper($xref2)]));
277
                    } else {
278
                        /* I18N: placeholders are GEDCOM XREFs, such as R123 */
279
                        $errors[] = $this->checkLinkMessage($tree, $type1, $xref1, $type2, $xref2) . ' ' . I18N::translate('%1$s does not exist.', $this->checkLink($tree, $xref2));
280
                    }
281
                } elseif ($type2 === 'SOUR' && $type1 === 'NOTE') {
282
                    // Notes are intended to add explanations and comments to other records. They should not have their own sources.
283
                } elseif ($type2 === 'SOUR' && $type1 === 'OBJE') {
284
                    // Media objects are intended to illustrate other records, facts, and source/citations. They should not have their own sources.
285
                } elseif ($type2 === 'OBJE' && $type1 === 'REPO') {
286
                    $warnings[] =
287
                        $this->checkLinkMessage($tree, $type1, $xref1, $type2, $xref2) .
288
                        ' ' .
289
                        I18N::translate('This type of link is not allowed here.');
290
                } elseif (!array_key_exists($type1, $RECORD_LINKS) || !in_array($type2, $RECORD_LINKS[$type1], true) || !array_key_exists($type2, $XREF_LINKS)) {
291
                    $errors[] =
292
                        $this->checkLinkMessage($tree, $type1, $xref1, $type2, $xref2) .
293
                        ' ' .
294
                        I18N::translate('This type of link is not allowed here.');
295
                } elseif ($XREF_LINKS[$type2] !== $type3) {
296
                    // Target XREF does exist - but is invalid
297
                    $errors[] =
298
                        $this->checkLinkMessage($tree, $type1, $xref1, $type2, $xref2) . ' ' .
299
                        /* I18N: %1$s is an internal ID number such as R123. %2$s and %3$s are record types, such as INDI or SOUR */
300
                        I18N::translate('%1$s is a %2$s but a %3$s is expected.', $this->checkLink($tree, $xref2), $this->formatType($type3), $this->formatType($type2));
301
                } elseif (
302
                    $this->checkReverseLink($type2, $all_links, $xref1, $xref2, 'FAMC', ['CHIL']) ||
303
                    $this->checkReverseLink($type2, $all_links, $xref1, $xref2, 'FAMS', ['HUSB', 'WIFE']) ||
304
                    $this->checkReverseLink($type2, $all_links, $xref1, $xref2, 'CHIL', ['FAMC']) ||
305
                    $this->checkReverseLink($type2, $all_links, $xref1, $xref2, 'HUSB', ['FAMS']) ||
306
                    $this->checkReverseLink($type2, $all_links, $xref1, $xref2, 'WIFE', ['FAMS'])
307
                ) {
308
                    /* I18N: %1$s and %2$s are internal ID numbers such as R123 */
309
                    $errors[] = $this->checkLinkMessage($tree, $type1, $xref1, $type2, $xref2) . ' ' . I18N::translate('%1$s does not have a link back to %2$s.', $this->checkLink($tree, $xref2), $this->checkLink($tree, $xref1));
310
                }
311
            }
312
        }
313
314
        $title = I18N::translate('Check for errors') . ' — ' . e($tree->title());
315
316
        return $this->viewResponse('admin/trees-check', [
317
            'errors'   => $errors,
318
            'title'    => $title,
319
            'tree'     => $tree,
320
            'warnings' => $warnings,
321
        ]);
322
    }
323
324
    /**
325
     * @param ServerRequestInterface $request
326
     *
327
     * @return ResponseInterface
328
     */
329
    public function duplicates(ServerRequestInterface $request): ResponseInterface
330
    {
331
        $tree = $request->getAttribute('tree');
332
        assert($tree instanceof Tree);
333
334
        $duplicates = $this->duplicateRecords($tree);
335
        $title      = I18N::translate('Find duplicates') . ' — ' . e($tree->title());
336
337
        return $this->viewResponse('admin/trees-duplicates', [
338
            'duplicates' => $duplicates,
339
            'title'      => $title,
340
            'tree'       => $tree,
341
        ]);
342
    }
343
344
    /**
345
     * @param ServerRequestInterface $request
346
     *
347
     * @return ResponseInterface
348
     */
349
    public function importAction(ServerRequestInterface $request): ResponseInterface
350
    {
351
        $tree = $request->getAttribute('tree');
352
        assert($tree instanceof Tree);
353
354
        $params             = $request->getParsedBody();
355
        $source             = $params['source'];
356
        $keep_media         = (bool) ($params['keep_media'] ?? false);
357
        $WORD_WRAPPED_NOTES = (bool) ($params['WORD_WRAPPED_NOTES'] ?? false);
358
        $GEDCOM_MEDIA_PATH  = $params['GEDCOM_MEDIA_PATH'];
359
360
        // Save these choices as defaults
361
        $tree->setPreference('keep_media', $keep_media ? '1' : '0');
362
        $tree->setPreference('WORD_WRAPPED_NOTES', $WORD_WRAPPED_NOTES ? '1' : '0');
363
        $tree->setPreference('GEDCOM_MEDIA_PATH', $GEDCOM_MEDIA_PATH);
364
365
        if ($source === 'client') {
366
            $upload = $request->getUploadedFiles()['tree_name'] ?? null;
367
368
            if ($upload instanceof UploadedFile) {
369
                if ($upload->getError() === UPLOAD_ERR_OK) {
370
                    $tree->importGedcomFile($upload->getStream(), basename($upload->getClientFilename()));
371
                } else {
372
                    FlashMessages::addMessage(Functions::fileUploadErrorText($upload->getError()), 'danger');
373
                }
374
            } else {
375
                FlashMessages::addMessage(I18N::translate('No GEDCOM file was received.'), 'danger');
376
            }
377
        }
378
379
        if ($source === 'server') {
380
            $basename = basename($params['tree_name'] ?? '');
381
382
            if ($basename) {
383
                $resource = $this->filesystem->readStream($basename);
384
                $stream   = app(StreamFactoryInterface::class)->createStreamFromResource($resource);
385
                $tree->importGedcomFile($stream, $basename);
386
            } else {
387
                FlashMessages::addMessage(I18N::translate('No GEDCOM file was received.'), 'danger');
388
            }
389
        }
390
391
        $url = route('manage-trees', ['tree' => $tree->name()]);
392
393
        return redirect($url);
394
    }
395
396
    /**
397
     * @param ServerRequestInterface $request
398
     *
399
     * @return ResponseInterface
400
     */
401
    public function importForm(ServerRequestInterface $request): ResponseInterface
402
    {
403
        $tree = $request->getAttribute('tree');
404
        assert($tree instanceof Tree);
405
406
        $default_gedcom_file = $tree->getPreference('gedcom_filename');
407
        $gedcom_media_path   = $tree->getPreference('GEDCOM_MEDIA_PATH');
408
        $gedcom_files        = $this->gedcomFiles();
409
410
        $title = I18N::translate('Import a GEDCOM file') . ' — ' . e($tree->title());
411
412
        return $this->viewResponse('admin/trees-import', [
413
            'data_folder'         => app('filesystem_description'),
414
            'default_gedcom_file' => $default_gedcom_file,
415
            'gedcom_files'        => $gedcom_files,
416
            'gedcom_media_path'   => $gedcom_media_path,
417
            'title'               => $title,
418
            'tree'                => $tree,
419
        ]);
420
    }
421
422
    /**
423
     * @param ServerRequestInterface $request
424
     *
425
     * @return ResponseInterface
426
     */
427
    public function index(ServerRequestInterface $request): ResponseInterface
428
    {
429
        $tree = $request->getAttribute('tree');
430
431
        $multiple_tree_threshold = (int) Site::getPreference('MULTIPLE_TREE_THRESHOLD', self::MULTIPLE_TREE_THRESHOLD);
432
        $gedcom_files            = $this->gedcomFiles();
433
434
        $all_trees = $this->tree_service->all();
435
436
        // On sites with hundreds or thousands of trees, this page becomes very large.
437
        // Just show the current tree, the default tree, and un-imported trees
438
        if ($all_trees->count() >= $multiple_tree_threshold) {
439
            $default   = Site::getPreference('DEFAULT_GEDCOM');
440
            $all_trees = $all_trees->filter(static function (Tree $x) use ($tree, $default): bool {
441
                if ($x->getPreference('imported') === '0') {
442
                    return true;
443
                }
444
                if ($tree instanceof Tree && $tree->id() === $x->id()) {
445
                    return true;
446
                }
447
448
                return $x->name() === $default;
449
            });
450
        }
451
452
        $title = I18N::translate('Manage family trees');
453
454
        $base_url = app(ServerRequestInterface::class)->getAttribute('base_url');
455
456
        return $this->viewResponse('admin/trees', [
457
            'all_trees'               => $all_trees,
458
            'base_url'                => $base_url,
459
            'gedcom_files'            => $gedcom_files,
460
            'multiple_tree_threshold' => $multiple_tree_threshold,
461
            'title'                   => $title,
462
            'tree'                    => $tree,
463
        ]);
464
    }
465
466
    /**
467
     * @param ServerRequestInterface $request
468
     *
469
     * @return ResponseInterface
470
     */
471
    public function merge(ServerRequestInterface $request): ResponseInterface
472
    {
473
        $params     = $request->getQueryParams();
474
        $tree1_name = $params['tree1_name'] ?? '';
475
        $tree2_name = $params['tree2_name'] ?? '';
476
477
        $tree1 = $this->tree_service->all()->get($tree1_name);
478
        $tree2 = $this->tree_service->all()->get($tree2_name);
479
480
        if ($tree1 !== null && $tree2 !== null && $tree1->id() !== $tree2->id()) {
481
            $xrefs = $this->countCommonXrefs($tree1, $tree2);
482
        } else {
483
            $xrefs = 0;
484
        }
485
486
        $title = I18N::translate(I18N::translate('Merge family trees'));
487
488
        return $this->viewResponse('admin/trees-merge', [
489
            'tree_list' => $this->tree_service->titles(),
490
            'tree1'     => $tree1,
491
            'tree2'     => $tree2,
492
            'title'     => $title,
493
            'xrefs'     => $xrefs,
494
        ]);
495
    }
496
497
    /**
498
     * @param ServerRequestInterface $request
499
     *
500
     * @return ResponseInterface
501
     */
502
    public function mergeAction(ServerRequestInterface $request): ResponseInterface
503
    {
504
        $params     = $request->getParsedBody();
505
        $tree1_name = $params['tree1_name'] ?? '';
506
        $tree2_name = $params['tree2_name'] ?? '';
507
508
        $tree1 = $this->tree_service->all()->get($tree1_name);
509
        $tree2 = $this->tree_service->all()->get($tree2_name);
510
511
        if ($tree1 instanceof Tree && $tree2 instanceof Tree && $tree1 !== $tree2 && $this->countCommonXrefs($tree1, $tree2) === 0) {
512
            (new Builder(DB::connection()))->from('individuals')->insertUsing([
513
                'i_file',
514
                'i_id',
515
                'i_rin',
516
                'i_sex',
517
                'i_gedcom',
518
            ], static function (Builder $query) use ($tree1, $tree2): void {
519
                $query->select([
520
                    new Expression($tree2->id()),
521
                    'i_id',
522
                    'i_rin',
523
                    'i_sex',
524
                    'i_gedcom',
525
                ])->from('individuals')
526
                    ->where('i_file', '=', $tree1->id());
527
            });
528
529
            (new Builder(DB::connection()))->from('families')->insertUsing([
530
                'f_file',
531
                'f_id',
532
                'f_husb',
533
                'f_wife',
534
                'f_gedcom',
535
                'f_numchil',
536
            ], static function (Builder $query) use ($tree1, $tree2): void {
537
                $query->select([
538
                    new Expression($tree2->id()),
539
                    'f_id',
540
                    'f_husb',
541
                    'f_wife',
542
                    'f_gedcom',
543
                    'f_numchil',
544
                ])->from('families')
545
                    ->where('f_file', '=', $tree1->id());
546
            });
547
548
            (new Builder(DB::connection()))->from('sources')->insertUsing([
549
                's_file',
550
                's_id',
551
                's_name',
552
                's_gedcom',
553
            ], static function (Builder $query) use ($tree1, $tree2): void {
554
                $query->select([
555
                    new Expression($tree2->id()),
556
                    's_id',
557
                    's_name',
558
                    's_gedcom',
559
                ])->from('sources')
560
                    ->where('s_file', '=', $tree1->id());
561
            });
562
563
            (new Builder(DB::connection()))->from('media')->insertUsing([
564
                'm_file',
565
                'm_id',
566
                'm_gedcom',
567
            ], static function (Builder $query) use ($tree1, $tree2): void {
568
                $query->select([
569
                    new Expression($tree2->id()),
570
                    'm_id',
571
                    'm_gedcom',
572
                ])->from('media')
573
                    ->where('m_file', '=', $tree1->id());
574
            });
575
576
            (new Builder(DB::connection()))->from('media_file')->insertUsing([
577
                'm_file',
578
                'm_id',
579
                'multimedia_file_refn',
580
                'multimedia_format',
581
                'source_media_type',
582
                'descriptive_title',
583
            ], static function (Builder $query) use ($tree1, $tree2): void {
584
                $query->select([
585
                    new Expression($tree2->id()),
586
                    'm_id',
587
                    'multimedia_file_refn',
588
                    'multimedia_format',
589
                    'source_media_type',
590
                    'descriptive_title',
591
                ])->from('media_file')
592
                    ->where('m_file', '=', $tree1->id());
593
            });
594
595
            (new Builder(DB::connection()))->from('other')->insertUsing([
596
                'o_file',
597
                'o_id',
598
                'o_type',
599
                'o_gedcom',
600
            ], static function (Builder $query) use ($tree1, $tree2): void {
601
                $query->select([
602
                    new Expression($tree2->id()),
603
                    'o_id',
604
                    'o_type',
605
                    'o_gedcom',
606
                ])->from('other')
607
                    ->whereNotIn('o_type', ['HEAD', 'TRLR'])
608
                    ->where('o_file', '=', $tree1->id());
609
            });
610
611
            (new Builder(DB::connection()))->from('name')->insertUsing([
612
                'n_file',
613
                'n_id',
614
                'n_num',
615
                'n_type',
616
                'n_sort',
617
                'n_full',
618
                'n_surname',
619
                'n_surn',
620
                'n_givn',
621
                'n_soundex_givn_std',
622
                'n_soundex_surn_std',
623
                'n_soundex_givn_dm',
624
                'n_soundex_surn_dm',
625
            ], static function (Builder $query) use ($tree1, $tree2): void {
626
                $query->select([
627
                    new Expression($tree2->id()),
628
                    'n_id',
629
                    'n_num',
630
                    'n_type',
631
                    'n_sort',
632
                    'n_full',
633
                    'n_surname',
634
                    'n_surn',
635
                    'n_givn',
636
                    'n_soundex_givn_std',
637
                    'n_soundex_surn_std',
638
                    'n_soundex_givn_dm',
639
                    'n_soundex_surn_dm',
640
                ])->from('name')
641
                    ->where('n_file', '=', $tree1->id());
642
            });
643
644
            (new Builder(DB::connection()))->from('dates')->insertUsing([
645
                'd_file',
646
                'd_gid',
647
                'd_day',
648
                'd_month',
649
                'd_mon',
650
                'd_year',
651
                'd_julianday1',
652
                'd_julianday2',
653
                'd_fact',
654
                'd_type',
655
            ], static function (Builder $query) use ($tree1, $tree2): void {
656
                $query->select([
657
                    new Expression($tree2->id()),
658
                    'd_gid',
659
                    'd_day',
660
                    'd_month',
661
                    'd_mon',
662
                    'd_year',
663
                    'd_julianday1',
664
                    'd_julianday2',
665
                    'd_fact',
666
                    'd_type',
667
                ])->from('dates')
668
                    ->where('d_file', '=', $tree1->id());
669
            });
670
671
            (new Builder(DB::connection()))->from('link')->insertUsing([
672
                'l_file',
673
                'l_from',
674
                'l_type',
675
                'l_to',
676
            ], static function (Builder $query) use ($tree1, $tree2): void {
677
                $query->select([
678
                    new Expression($tree2->id()),
679
                    'l_from',
680
                    'l_type',
681
                    'l_to',
682
                ])->from('link')
683
                    ->where('l_file', '=', $tree1->id());
684
            });
685
686
            FlashMessages::addMessage(I18N::translate('The family trees have been merged successfully.'), 'success');
687
688
            $url = route('manage-trees', ['tree' => $tree2->name()]);
689
        } else {
690
            $url = route('admin-trees-merge', [
691
                'tree1_name' => $tree1->name(),
692
                'tree2_name' => $tree2->name(),
693
            ]);
694
        }
695
696
        return redirect($url);
697
    }
698
699
    /**
700
     * @param ServerRequestInterface $request
701
     *
702
     * @return ResponseInterface
703
     */
704
    public function places(ServerRequestInterface $request): ResponseInterface
705
    {
706
        $tree = $request->getAttribute('tree');
707
        assert($tree instanceof Tree);
708
709
        $params  = $request->getQueryParams();
710
        $search  = $params['search'] ?? '';
711
        $replace = $params['replace'] ?? '';
712
713
        if ($search !== '' && $replace !== '') {
714
            $changes = $this->changePlacesPreview($tree, $search, $replace);
715
        } else {
716
            $changes = [];
717
        }
718
719
        /* I18N: Renumber the records in a family tree */
720
        $title = I18N::translate('Update place names') . ' — ' . e($tree->title());
721
722
        return $this->viewResponse('admin/trees-places', [
723
            'changes' => $changes,
724
            'replace' => $replace,
725
            'search'  => $search,
726
            'title'   => $title,
727
        ]);
728
    }
729
730
    /**
731
     * @param ServerRequestInterface $request
732
     *
733
     * @return ResponseInterface
734
     */
735
    public function placesAction(ServerRequestInterface $request): ResponseInterface
736
    {
737
        $tree = $request->getAttribute('tree');
738
        assert($tree instanceof Tree);
739
740
        $params  = $request->getQueryParams();
741
        $search  = $params['search'] ?? '';
742
        $replace = $params['replace'] ?? '';
743
744
        $changes = $this->changePlacesUpdate($tree, $search, $replace);
745
746
        $feedback = I18N::translate('The following places have been changed:') . '<ul>';
747
        foreach ($changes as $old_place => $new_place) {
748
            $feedback .= '<li>' . e($old_place) . ' &rarr; ' . e($new_place) . '</li>';
749
        }
750
        $feedback .= '</ul>';
751
752
        FlashMessages::addMessage($feedback, 'success');
753
754
        $url = route('admin-trees-places', [
755
            'tree'    => $tree->name(),
756
            'replace' => $replace,
757
            'search'  => $search,
758
        ]);
759
760
        return redirect($url);
761
    }
762
763
    /**
764
     * @param ServerRequestInterface $request
765
     *
766
     * @return ResponseInterface
767
     */
768
    public function preferences(ServerRequestInterface $request): ResponseInterface
769
    {
770
        $tree = $request->getAttribute('tree');
771
        assert($tree instanceof Tree);
772
773
        $french_calendar_start    = new Date('22 SEP 1792');
774
        $french_calendar_end      = new Date('31 DEC 1805');
775
        $gregorian_calendar_start = new Date('15 OCT 1582');
776
777
        $surname_list_styles = [
778
            /* I18N: Layout option for lists of names */
779
            'style1' => I18N::translate('list'),
780
            /* I18N: Layout option for lists of names */
781
            'style2' => I18N::translate('table'),
782
            /* I18N: Layout option for lists of names */
783
            'style3' => I18N::translate('tag cloud'),
784
        ];
785
786
        $page_layouts = [
787
            /* I18N: page orientation */
788
            0 => I18N::translate('Portrait'),
789
            /* I18N: page orientation */
790
            1 => I18N::translate('Landscape'),
791
        ];
792
793
        $formats = [
794
            /* I18N: None of the other options */
795
            ''         => I18N::translate('none'),
796
            /* I18N: https://en.wikipedia.org/wiki/Markdown */
797
            'markdown' => I18N::translate('markdown'),
798
        ];
799
800
        $source_types = [
801
            0 => I18N::translate('none'),
802
            1 => I18N::translate('facts'),
803
            2 => I18N::translate('records'),
804
        ];
805
806
        $theme_options = $this->themeOptions();
807
808
        $privacy_options = [
809
            Auth::PRIV_USER => I18N::translate('Show to members'),
810
            Auth::PRIV_NONE => I18N::translate('Show to managers'),
811
            Auth::PRIV_HIDE => I18N::translate('Hide from everyone'),
812
        ];
813
814
        $tags = array_unique(array_merge(
815
            explode(',', $tree->getPreference('INDI_FACTS_ADD')),
816
            explode(',', $tree->getPreference('INDI_FACTS_UNIQUE')),
817
            explode(',', $tree->getPreference('FAM_FACTS_ADD')),
818
            explode(',', $tree->getPreference('FAM_FACTS_UNIQUE')),
819
            explode(',', $tree->getPreference('NOTE_FACTS_ADD')),
820
            explode(',', $tree->getPreference('NOTE_FACTS_UNIQUE')),
821
            explode(',', $tree->getPreference('SOUR_FACTS_ADD')),
822
            explode(',', $tree->getPreference('SOUR_FACTS_UNIQUE')),
823
            explode(',', $tree->getPreference('REPO_FACTS_ADD')),
824
            explode(',', $tree->getPreference('REPO_FACTS_UNIQUE')),
825
            ['SOUR', 'REPO', 'OBJE', '_PRIM', 'NOTE', 'SUBM', 'SUBN', '_UID', 'CHAN']
826
        ));
827
828
        $all_tags = [];
829
        foreach ($tags as $tag) {
830
            if ($tag) {
831
                $all_tags[$tag] = GedcomTag::getLabel($tag);
832
            }
833
        }
834
835
        uasort($all_tags, '\Fisharebest\Webtrees\I18N::strcasecmp');
836
837
        // For historical reasons, we have two fields in one
838
        $calendar_formats = explode('_and_', $tree->getPreference('CALENDAR_FORMAT') . '_and_');
839
840
        // Split into separate fields
841
        $relatives_events = explode(',', $tree->getPreference('SHOW_RELATIVES_EVENTS'));
842
843
        $pedigree_individual = Individual::getInstance($tree->getPreference('PEDIGREE_ROOT_ID'), $tree);
844
845
        $members = $this->user_service->all()->filter(static function (UserInterface $user) use ($tree): bool {
846
            return Auth::isMember($tree, $user);
847
        });
848
849
        $all_fam_facts  = GedcomTag::getPicklistFacts('FAM');
850
        $all_indi_facts = GedcomTag::getPicklistFacts('INDI');
851
        $all_name_facts = GedcomTag::getPicklistFacts('NAME');
852
        $all_plac_facts = GedcomTag::getPicklistFacts('PLAC');
853
        $all_repo_facts = GedcomTag::getPicklistFacts('REPO');
854
        $all_sour_facts = GedcomTag::getPicklistFacts('SOUR');
855
856
        $all_surname_traditions = SurnameTradition::allDescriptions();
857
858
        $tree_count = $this->tree_service->all()->count();
859
860
        $title = I18N::translate('Preferences') . ' — ' . e($tree->title());
861
862
        $base_url = app(ServerRequestInterface::class)->getAttribute('base_url');
863
864
        return $this->viewResponse('admin/trees-preferences', [
865
            'all_fam_facts'            => $all_fam_facts,
866
            'all_indi_facts'           => $all_indi_facts,
867
            'all_name_facts'           => $all_name_facts,
868
            'all_plac_facts'           => $all_plac_facts,
869
            'all_repo_facts'           => $all_repo_facts,
870
            'all_sour_facts'           => $all_sour_facts,
871
            'all_surname_traditions'   => $all_surname_traditions,
872
            'base_url'                 => $base_url,
873
            'calendar_formats'         => $calendar_formats,
874
            'data_folder'              => app('filesystem_description'),
875
            'formats'                  => $formats,
876
            'french_calendar_end'      => $french_calendar_end,
877
            'french_calendar_start'    => $french_calendar_start,
878
            'gregorian_calendar_start' => $gregorian_calendar_start,
879
            'members'                  => $members,
880
            'page_layouts'             => $page_layouts,
881
            'pedigree_individual'      => $pedigree_individual,
882
            'privacy_options'          => $privacy_options,
883
            'relatives_events'         => $relatives_events,
884
            'source_types'             => $source_types,
885
            'surname_list_styles'      => $surname_list_styles,
886
            'theme_options'            => $theme_options,
887
            'title'                    => $title,
888
            'tree'                     => $tree,
889
            'tree_count'               => $tree_count,
890
        ]);
891
    }
892
893
    /**
894
     * @param ServerRequestInterface $request
895
     *
896
     * @return ResponseInterface
897
     */
898
    public function renumber(ServerRequestInterface $request): ResponseInterface
899
    {
900
        $tree = $request->getAttribute('tree');
901
        assert($tree instanceof Tree);
902
903
        $xrefs = $this->duplicateXrefs($tree);
904
905
        /* I18N: Renumber the records in a family tree */
906
        $title = I18N::translate('Renumber family tree') . ' — ' . e($tree->title());
907
908
        return $this->viewResponse('admin/trees-renumber', [
909
            'title' => $title,
910
            'tree'  => $tree,
911
            'xrefs' => $xrefs,
912
        ]);
913
    }
914
915
    /**
916
     * @param ServerRequestInterface $request
917
     *
918
     * @return ResponseInterface
919
     */
920
    public function preferencesUpdate(ServerRequestInterface $request): ResponseInterface
921
    {
922
        $tree = $request->getAttribute('tree');
923
        assert($tree instanceof Tree);
924
925
        $tree->setPreference('ADVANCED_NAME_FACTS', implode(',', $request->getParsedBody()['ADVANCED_NAME_FACTS'] ?? []));
926
        $tree->setPreference('ADVANCED_PLAC_FACTS', implode(',', $request->getParsedBody()['ADVANCED_PLAC_FACTS'] ?? []));
927
        // For backwards compatibility with webtrees 1.x we store the two calendar formats in one variable
928
        // e.g. "gregorian_and_jewish"
929
        $tree->setPreference('CALENDAR_FORMAT', implode('_and_', array_unique([
930
            $request->getParsedBody()['CALENDAR_FORMAT0'] ?? 'none',
931
            $request->getParsedBody()['CALENDAR_FORMAT1'] ?? 'none',
932
        ])));
933
        $tree->setPreference('CHART_BOX_TAGS', implode(',', $request->getParsedBody()['CHART_BOX_TAGS'] ?? []));
934
        $tree->setPreference('CONTACT_USER_ID', $request->getParsedBody()['CONTACT_USER_ID'] ?? '');
935
        $tree->setPreference('EXPAND_NOTES', $request->getParsedBody()['EXPAND_NOTES'] ?? '');
936
        $tree->setPreference('EXPAND_SOURCES', $request->getParsedBody()['EXPAND_SOURCES'] ?? '');
937
        $tree->setPreference('FAM_FACTS_ADD', implode(',', $request->getParsedBody()['FAM_FACTS_ADD'] ?? []));
938
        $tree->setPreference('FAM_FACTS_QUICK', implode(',', $request->getParsedBody()['FAM_FACTS_QUICK'] ?? []));
939
        $tree->setPreference('FAM_FACTS_UNIQUE', implode(',', $request->getParsedBody()['FAM_FACTS_UNIQUE'] ?? []));
940
        $tree->setPreference('FULL_SOURCES', $request->getParsedBody()['FULL_SOURCES'] ?? '');
941
        $tree->setPreference('FORMAT_TEXT', $request->getParsedBody()['FORMAT_TEXT'] ?? '');
942
        $tree->setPreference('GENERATE_UIDS', $request->getParsedBody()['GENERATE_UIDS'] ?? '');
943
        $tree->setPreference('HIDE_GEDCOM_ERRORS', $request->getParsedBody()['HIDE_GEDCOM_ERRORS'] ?? '');
944
        $tree->setPreference('INDI_FACTS_ADD', implode(',', $request->getParsedBody()['INDI_FACTS_ADD'] ?? []));
945
        $tree->setPreference('INDI_FACTS_QUICK', implode(',', $request->getParsedBody()['INDI_FACTS_QUICK'] ?? []));
946
        $tree->setPreference('INDI_FACTS_UNIQUE', implode(',', $request->getParsedBody()['INDI_FACTS_UNIQUE'] ?? []));
947
        $tree->setPreference('LANGUAGE', $request->getParsedBody()['LANGUAGE'] ?? '');
948
        $tree->setPreference('MEDIA_UPLOAD', $request->getParsedBody()['MEDIA_UPLOAD'] ?? '');
949
        $tree->setPreference('META_DESCRIPTION', $request->getParsedBody()['META_DESCRIPTION'] ?? '');
950
        $tree->setPreference('META_TITLE', $request->getParsedBody()['META_TITLE'] ?? '');
951
        $tree->setPreference('NO_UPDATE_CHAN', $request->getParsedBody()['NO_UPDATE_CHAN'] ?? '');
952
        $tree->setPreference('PEDIGREE_ROOT_ID', $request->getParsedBody()['PEDIGREE_ROOT_ID'] ?? '');
953
        $tree->setPreference('PREFER_LEVEL2_SOURCES', $request->getParsedBody()['PREFER_LEVEL2_SOURCES'] ?? '');
954
        $tree->setPreference('QUICK_REQUIRED_FACTS', implode(',', $request->getParsedBody()['QUICK_REQUIRED_FACTS'] ?? []));
955
        $tree->setPreference('QUICK_REQUIRED_FAMFACTS', implode(',', $request->getParsedBody()['QUICK_REQUIRED_FAMFACTS'] ?? []));
956
        $tree->setPreference('REPO_FACTS_ADD', implode(',', $request->getParsedBody()['REPO_FACTS_ADD'] ?? []));
957
        $tree->setPreference('REPO_FACTS_QUICK', implode(',', $request->getParsedBody()['REPO_FACTS_QUICK'] ?? []));
958
        $tree->setPreference('REPO_FACTS_UNIQUE', implode(',', $request->getParsedBody()['REPO_FACTS_UNIQUE'] ?? []));
959
        $tree->setPreference('SHOW_COUNTER', $request->getParsedBody()['SHOW_COUNTER'] ?? '');
960
        $tree->setPreference('SHOW_EST_LIST_DATES', $request->getParsedBody()['SHOW_EST_LIST_DATES'] ?? '');
961
        $tree->setPreference('SHOW_FACT_ICONS', $request->getParsedBody()['SHOW_FACT_ICONS'] ?? '');
962
        $tree->setPreference('SHOW_GEDCOM_RECORD', $request->getParsedBody()['SHOW_GEDCOM_RECORD'] ?? '');
963
        $tree->setPreference('SHOW_HIGHLIGHT_IMAGES', $request->getParsedBody()['SHOW_HIGHLIGHT_IMAGES'] ?? '');
964
        $tree->setPreference('SHOW_LAST_CHANGE', $request->getParsedBody()['SHOW_LAST_CHANGE'] ?? '');
965
        $tree->setPreference('SHOW_MEDIA_DOWNLOAD', $request->getParsedBody()['SHOW_MEDIA_DOWNLOAD'] ?? '');
966
        $tree->setPreference('SHOW_NO_WATERMARK', $request->getParsedBody()['SHOW_NO_WATERMARK'] ?? '');
967
        $tree->setPreference('SHOW_PARENTS_AGE', $request->getParsedBody()['SHOW_PARENTS_AGE'] ?? '');
968
        $tree->setPreference('SHOW_PEDIGREE_PLACES', $request->getParsedBody()['SHOW_PEDIGREE_PLACES'] ?? '');
969
        $tree->setPreference('SHOW_PEDIGREE_PLACES_SUFFIX', $request->getParsedBody()['SHOW_PEDIGREE_PLACES_SUFFIX'] ?? '');
970
        $tree->setPreference('SHOW_RELATIVES_EVENTS', implode(',', $request->getParsedBody()['SHOW_RELATIVES_EVENTS'] ?? []));
971
        $tree->setPreference('SOUR_FACTS_ADD', implode(',', $request->getParsedBody()['SOUR_FACTS_ADD'] ?? []));
972
        $tree->setPreference('SOUR_FACTS_QUICK', implode(',', $request->getParsedBody()['SOUR_FACTS_QUICK'] ?? []));
973
        $tree->setPreference('SOUR_FACTS_UNIQUE', implode(',', $request->getParsedBody()['SOUR_FACTS_UNIQUE'] ?? []));
974
        $tree->setPreference('SUBLIST_TRIGGER_I', $request->getParsedBody()['SUBLIST_TRIGGER_I'] ?? '200');
975
        $tree->setPreference('SURNAME_LIST_STYLE', $request->getParsedBody()['SURNAME_LIST_STYLE'] ?? '');
976
        $tree->setPreference('SURNAME_TRADITION', $request->getParsedBody()['SURNAME_TRADITION'] ?? '');
977
        $tree->setPreference('USE_SILHOUETTE', $request->getParsedBody()['USE_SILHOUETTE'] ?? '');
978
        $tree->setPreference('WEBMASTER_USER_ID', $request->getParsedBody()['WEBMASTER_USER_ID'] ?? '');
979
        $tree->setPreference('title', $request->getParsedBody()['title'] ?? '');
980
981
        // Only accept valid folders for MEDIA_DIRECTORY
982
        $MEDIA_DIRECTORY = $request->getParsedBody()['MEDIA_DIRECTORY'] ?? '';
983
        $MEDIA_DIRECTORY = preg_replace('/[:\/\\\\]+/', '/', $MEDIA_DIRECTORY);
984
        $MEDIA_DIRECTORY = trim($MEDIA_DIRECTORY, '/') . '/';
985
986
        $tree->setPreference('MEDIA_DIRECTORY', $MEDIA_DIRECTORY);
987
988
        $gedcom = $request->getParsedBody()['gedcom'] ?? '';
989
990
        if ($gedcom !== '' && $gedcom !== $tree->name()) {
991
            try {
992
                DB::table('gedcom')
993
                    ->where('gedcom_id', '=', $tree->id())
994
                    ->update(['gedcom_name' => $gedcom]);
995
996
                DB::table('site_setting')
997
                    ->where('setting_name', '=', 'DEFAULT_GEDCOM')
998
                    ->where('setting_value', '=', $tree->name())
999
                    ->update(['setting_value' => $gedcom]);
1000
            } catch (Exception $ex) {
1001
                // Probably a duplicate name.
1002
            }
1003
        }
1004
1005
        FlashMessages::addMessage(I18N::translate('The preferences for the family tree “%s” have been updated.', e($tree->title())), 'success');
1006
1007
        // Coming soon...
1008
        $all_trees = $request->getParsedBody()['all_trees'] ?? '';
1009
        $new_trees = $request->getParsedBody()['new_trees'] ?? '';
1010
1011
        if ($all_trees === 'on') {
1012
            FlashMessages::addMessage(I18N::translate('The preferences for all family trees have been updated.'), 'success');
1013
        }
1014
1015
        if ($new_trees === 'on') {
1016
            FlashMessages::addMessage(I18N::translate('The preferences for new family trees have been updated.'), 'success');
1017
        }
1018
1019
        $url = route('manage-trees', ['tree' => $tree->name()]);
1020
1021
        return redirect($url);
1022
    }
1023
1024
    /**
1025
     * @param ServerRequestInterface $request
1026
     *
1027
     * @return ResponseInterface
1028
     */
1029
    public function renumberAction(ServerRequestInterface $request): ResponseInterface
1030
    {
1031
        $tree = $request->getAttribute('tree');
1032
        assert($tree instanceof Tree);
1033
1034
        $xrefs = $this->duplicateXrefs($tree);
1035
1036
        foreach ($xrefs as $old_xref => $type) {
1037
            $new_xref = $tree->getNewXref();
1038
            switch ($type) {
1039
                case 'INDI':
1040
                    DB::table('individuals')
1041
                        ->where('i_file', '=', $tree->id())
1042
                        ->where('i_id', '=', $old_xref)
1043
                        ->update([
1044
                            'i_id'     => $new_xref,
1045
                            'i_gedcom' => new Expression("REPLACE(i_gedcom, '0 @$old_xref@ INDI', '0 @$new_xref@ INDI')"),
1046
                        ]);
1047
1048
                    DB::table('families')
1049
                        ->where('f_husb', '=', $old_xref)
1050
                        ->where('f_file', '=', $tree->id())
1051
                        ->update([
1052
                            'f_husb'   => $new_xref,
1053
                            'f_gedcom' => new Expression("REPLACE(f_gedcom, ' HUSB @$old_xref@', ' HUSB @$new_xref@')"),
1054
                        ]);
1055
1056
                    DB::table('families')
1057
                        ->where('f_wife', '=', $old_xref)
1058
                        ->where('f_file', '=', $tree->id())
1059
                        ->update([
1060
                            'f_wife'   => $new_xref,
1061
                            'f_gedcom' => new Expression("REPLACE(f_gedcom, ' WIFE @$old_xref@', ' WIFE @$new_xref@')"),
1062
                        ]);
1063
1064
                    // Other links from families to individuals
1065
                    foreach (['CHIL', 'ASSO', '_ASSO'] as $tag) {
1066
                        DB::table('families')
1067
                            ->join('link', static function (JoinClause $join): void {
1068
                                $join
1069
                                    ->on('l_file', '=', 'f_file')
1070
                                    ->on('l_from', '=', 'f_id');
1071
                            })
1072
                            ->where('l_to', '=', $old_xref)
1073
                            ->where('l_type', '=', $tag)
1074
                            ->where('f_file', '=', $tree->id())
1075
                            ->update([
1076
                                'f_gedcom' => new Expression("REPLACE(f_gedcom, ' $tag @$old_xref@', ' $tag @$new_xref@')"),
1077
                            ]);
1078
                    }
1079
1080
                    // Links from individuals to individuals
1081
                    foreach (['ALIA', 'ASSO', '_ASSO'] as $tag) {
1082
                        DB::table('individuals')
1083
                            ->join('link', static function (JoinClause $join): void {
1084
                                $join
1085
                                    ->on('l_file', '=', 'i_file')
1086
                                    ->on('l_from', '=', 'i_id');
1087
                            })
1088
                            ->where('link.l_to', '=', $old_xref)
1089
                            ->where('link.l_type', '=', $tag)
1090
                            ->where('i_file', '=', $tree->id())
1091
                            ->update([
1092
                                'i_gedcom' => new Expression("REPLACE(i_gedcom, ' $tag @$old_xref@', ' $tag @$new_xref@')"),
1093
                            ]);
1094
                    }
1095
1096
                    DB::table('placelinks')
1097
                        ->where('pl_file', '=', $tree->id())
1098
                        ->where('pl_gid', '=', $old_xref)
1099
                        ->update([
1100
                            'pl_gid' => $new_xref,
1101
                        ]);
1102
1103
                    DB::table('dates')
1104
                        ->where('d_file', '=', $tree->id())
1105
                        ->where('d_gid', '=', $old_xref)
1106
                        ->update([
1107
                            'd_gid' => $new_xref,
1108
                        ]);
1109
1110
                    DB::table('user_gedcom_setting')
1111
                        ->where('gedcom_id', '=', $tree->id())
1112
                        ->where('setting_value', '=', $old_xref)
1113
                        ->whereIn('setting_name', ['gedcomid', 'rootid'])
1114
                        ->update([
1115
                            'setting_value' => $new_xref,
1116
                        ]);
1117
                    break;
1118
1119
                case 'FAM':
1120
                    DB::table('families')
1121
                        ->where('f_file', '=', $tree->id())
1122
                        ->where('f_id', '=', $old_xref)
1123
                        ->update([
1124
                            'f_id'     => $new_xref,
1125
                            'f_gedcom' => new Expression("REPLACE(f_gedcom, '0 @$old_xref@ FAM', '0 @$new_xref@ FAM')"),
1126
                        ]);
1127
1128
                    // Links from individuals to families
1129
                    foreach (['FAMC', 'FAMS'] as $tag) {
1130
                        DB::table('individuals')
1131
                            ->join('link', static function (JoinClause $join): void {
1132
                                $join
1133
                                    ->on('l_file', '=', 'i_file')
1134
                                    ->on('l_from', '=', 'i_id');
1135
                            })
1136
                            ->where('l_to', '=', $old_xref)
1137
                            ->where('l_type', '=', $tag)
1138
                            ->where('i_file', '=', $tree->id())
1139
                            ->update([
1140
                                'i_gedcom' => new Expression("REPLACE(i_gedcom, ' $tag @$old_xref@', ' $tag @$new_xref@')"),
1141
                            ]);
1142
                    }
1143
1144
                    DB::table('placelinks')
1145
                        ->where('pl_file', '=', $tree->id())
1146
                        ->where('pl_gid', '=', $old_xref)
1147
                        ->update([
1148
                            'pl_gid' => $new_xref,
1149
                        ]);
1150
1151
                    DB::table('dates')
1152
                        ->where('d_file', '=', $tree->id())
1153
                        ->where('d_gid', '=', $old_xref)
1154
                        ->update([
1155
                            'd_gid' => $new_xref,
1156
                        ]);
1157
                    break;
1158
1159
                case 'SOUR':
1160
                    DB::table('sources')
1161
                        ->where('s_file', '=', $tree->id())
1162
                        ->where('s_id', '=', $old_xref)
1163
                        ->update([
1164
                            's_id'     => $new_xref,
1165
                            's_gedcom' => new Expression("REPLACE(s_gedcom, '0 @$old_xref@ SOUR', '0 @$new_xref@ SOUR')"),
1166
                        ]);
1167
1168
                    DB::table('individuals')
1169
                        ->join('link', static function (JoinClause $join): void {
1170
                            $join
1171
                                ->on('l_file', '=', 'i_file')
1172
                                ->on('l_from', '=', 'i_id');
1173
                        })
1174
                        ->where('l_to', '=', $old_xref)
1175
                        ->where('l_type', '=', 'SOUR')
1176
                        ->where('i_file', '=', $tree->id())
1177
                        ->update([
1178
                            'i_gedcom' => new Expression("REPLACE(i_gedcom, ' SOUR @$old_xref@', ' SOUR @$new_xref@')"),
1179
                        ]);
1180
1181
                    DB::table('families')
1182
                        ->join('link', static function (JoinClause $join): void {
1183
                            $join
1184
                                ->on('l_file', '=', 'f_file')
1185
                                ->on('l_from', '=', 'f_id');
1186
                        })
1187
                        ->where('l_to', '=', $old_xref)
1188
                        ->where('l_type', '=', 'SOUR')
1189
                        ->where('f_file', '=', $tree->id())
1190
                        ->update([
1191
                            'f_gedcom' => new Expression("REPLACE(f_gedcom, ' SOUR @$old_xref@', ' SOUR @$new_xref@')"),
1192
                        ]);
1193
1194
                    DB::table('media')
1195
                        ->join('link', static function (JoinClause $join): void {
1196
                            $join
1197
                                ->on('l_file', '=', 'm_file')
1198
                                ->on('l_from', '=', 'm_id');
1199
                        })
1200
                        ->where('l_to', '=', $old_xref)
1201
                        ->where('l_type', '=', 'SOUR')
1202
                        ->where('m_file', '=', $tree->id())
1203
                        ->update([
1204
                            'm_gedcom' => new Expression("REPLACE(m_gedcom, ' SOUR @$old_xref@', ' SOUR @$new_xref@')"),
1205
                        ]);
1206
1207
                    DB::table('other')
1208
                        ->join('link', static function (JoinClause $join): void {
1209
                            $join
1210
                                ->on('l_file', '=', 'o_file')
1211
                                ->on('l_from', '=', 'o_id');
1212
                        })
1213
                        ->where('l_to', '=', $old_xref)
1214
                        ->where('l_type', '=', 'SOUR')
1215
                        ->where('o_file', '=', $tree->id())
1216
                        ->update([
1217
                            'o_gedcom' => new Expression("REPLACE(o_gedcom, ' SOUR @$old_xref@', ' SOUR @$new_xref@')"),
1218
                        ]);
1219
                    break;
1220
                case 'REPO':
1221
                    DB::table('other')
1222
                        ->where('o_file', '=', $tree->id())
1223
                        ->where('o_id', '=', $old_xref)
1224
                        ->where('o_type', '=', 'REPO')
1225
                        ->update([
1226
                            'o_id'     => $new_xref,
1227
                            'o_gedcom' => new Expression("REPLACE(o_gedcom, '0 @$old_xref@ REPO', '0 @$new_xref@ REPO')"),
1228
                        ]);
1229
1230
                    DB::table('sources')
1231
                        ->join('link', static function (JoinClause $join): void {
1232
                            $join
1233
                                ->on('l_file', '=', 's_file')
1234
                                ->on('l_from', '=', 's_id');
1235
                        })
1236
                        ->where('l_to', '=', $old_xref)
1237
                        ->where('l_type', '=', 'REPO')
1238
                        ->where('s_file', '=', $tree->id())
1239
                        ->update([
1240
                            's_gedcom' => new Expression("REPLACE(s_gedcom, ' REPO @$old_xref@', ' REPO @$new_xref@')"),
1241
                        ]);
1242
                    break;
1243
1244
                case 'NOTE':
1245
                    DB::table('other')
1246
                        ->where('o_file', '=', $tree->id())
1247
                        ->where('o_id', '=', $old_xref)
1248
                        ->where('o_type', '=', 'NOTE')
1249
                        ->update([
1250
                            'o_id'     => $new_xref,
1251
                            'o_gedcom' => new Expression("REPLACE(o_gedcom, '0 @$old_xref@ NOTE', '0 @$new_xref@ NOTE')"),
1252
                        ]);
1253
1254
                    DB::table('individuals')
1255
                        ->join('link', static function (JoinClause $join): void {
1256
                            $join
1257
                                ->on('l_file', '=', 'i_file')
1258
                                ->on('l_from', '=', 'i_id');
1259
                        })
1260
                        ->where('l_to', '=', $old_xref)
1261
                        ->where('l_type', '=', 'NOTE')
1262
                        ->where('i_file', '=', $tree->id())
1263
                        ->update([
1264
                            'i_gedcom' => new Expression("REPLACE(i_gedcom, ' NOTE @$old_xref@', ' NOTE @$new_xref@')"),
1265
                        ]);
1266
1267
                    DB::table('families')
1268
                        ->join('link', static function (JoinClause $join): void {
1269
                            $join
1270
                                ->on('l_file', '=', 'f_file')
1271
                                ->on('l_from', '=', 'f_id');
1272
                        })
1273
                        ->where('l_to', '=', $old_xref)
1274
                        ->where('l_type', '=', 'NOTE')
1275
                        ->where('f_file', '=', $tree->id())
1276
                        ->update([
1277
                            'f_gedcom' => new Expression("REPLACE(f_gedcom, ' NOTE @$old_xref@', ' NOTE @$new_xref@')"),
1278
                        ]);
1279
1280
                    DB::table('media')
1281
                        ->join('link', static function (JoinClause $join): void {
1282
                            $join
1283
                                ->on('l_file', '=', 'm_file')
1284
                                ->on('l_from', '=', 'm_id');
1285
                        })
1286
                        ->where('l_to', '=', $old_xref)
1287
                        ->where('l_type', '=', 'NOTE')
1288
                        ->where('m_file', '=', $tree->id())
1289
                        ->update([
1290
                            'm_gedcom' => new Expression("REPLACE(m_gedcom, ' NOTE @$old_xref@', ' NOTE @$new_xref@')"),
1291
                        ]);
1292
1293
                    DB::table('sources')
1294
                        ->join('link', static function (JoinClause $join): void {
1295
                            $join
1296
                                ->on('l_file', '=', 's_file')
1297
                                ->on('l_from', '=', 's_id');
1298
                        })
1299
                        ->where('l_to', '=', $old_xref)
1300
                        ->where('l_type', '=', 'NOTE')
1301
                        ->where('s_file', '=', $tree->id())
1302
                        ->update([
1303
                            's_gedcom' => new Expression("REPLACE(s_gedcom, ' NOTE @$old_xref@', ' NOTE @$new_xref@')"),
1304
                        ]);
1305
1306
                    DB::table('other')
1307
                        ->join('link', static function (JoinClause $join): void {
1308
                            $join
1309
                                ->on('l_file', '=', 'o_file')
1310
                                ->on('l_from', '=', 'o_id');
1311
                        })
1312
                        ->where('l_to', '=', $old_xref)
1313
                        ->where('l_type', '=', 'NOTE')
1314
                        ->where('o_file', '=', $tree->id())
1315
                        ->update([
1316
                            'o_gedcom' => new Expression("REPLACE(o_gedcom, ' NOTE @$old_xref@', ' NOTE @$new_xref@')"),
1317
                        ]);
1318
                    break;
1319
1320
                case 'OBJE':
1321
                    DB::table('media')
1322
                        ->where('m_file', '=', $tree->id())
1323
                        ->where('m_id', '=', $old_xref)
1324
                        ->update([
1325
                            'm_id'     => $new_xref,
1326
                            'm_gedcom' => new Expression("REPLACE(m_gedcom, '0 @$old_xref@ OBJE', '0 @$new_xref@ OBJE')"),
1327
                        ]);
1328
1329
                    DB::table('media_file')
1330
                        ->where('m_file', '=', $tree->id())
1331
                        ->where('m_id', '=', $old_xref)
1332
                        ->update([
1333
                            'm_id' => $new_xref,
1334
                        ]);
1335
1336
                    DB::table('individuals')
1337
                        ->join('link', static function (JoinClause $join): void {
1338
                            $join
1339
                                ->on('l_file', '=', 'i_file')
1340
                                ->on('l_from', '=', 'i_id');
1341
                        })
1342
                        ->where('l_to', '=', $old_xref)
1343
                        ->where('l_type', '=', 'OBJE')
1344
                        ->where('i_file', '=', $tree->id())
1345
                        ->update([
1346
                            'i_gedcom' => new Expression("REPLACE(i_gedcom, ' OBJE @$old_xref@', ' OBJE @$new_xref@')"),
1347
                        ]);
1348
1349
                    DB::table('families')
1350
                        ->join('link', static function (JoinClause $join): void {
1351
                            $join
1352
                                ->on('l_file', '=', 'f_file')
1353
                                ->on('l_from', '=', 'f_id');
1354
                        })
1355
                        ->where('l_to', '=', $old_xref)
1356
                        ->where('l_type', '=', 'OBJE')
1357
                        ->where('f_file', '=', $tree->id())
1358
                        ->update([
1359
                            'f_gedcom' => new Expression("REPLACE(f_gedcom, ' OBJE @$old_xref@', ' OBJE @$new_xref@')"),
1360
                        ]);
1361
1362
                    DB::table('sources')
1363
                        ->join('link', static function (JoinClause $join): void {
1364
                            $join
1365
                                ->on('l_file', '=', 's_file')
1366
                                ->on('l_from', '=', 's_id');
1367
                        })
1368
                        ->where('l_to', '=', $old_xref)
1369
                        ->where('l_type', '=', 'OBJE')
1370
                        ->where('s_file', '=', $tree->id())
1371
                        ->update([
1372
                            's_gedcom' => new Expression("REPLACE(s_gedcom, ' OBJE @$old_xref@', ' OBJE @$new_xref@')"),
1373
                        ]);
1374
1375
                    DB::table('other')
1376
                        ->join('link', static function (JoinClause $join): void {
1377
                            $join
1378
                                ->on('l_file', '=', 'o_file')
1379
                                ->on('l_from', '=', 'o_id');
1380
                        })
1381
                        ->where('l_to', '=', $old_xref)
1382
                        ->where('l_type', '=', 'OBJE')
1383
                        ->where('o_file', '=', $tree->id())
1384
                        ->update([
1385
                            'o_gedcom' => new Expression("REPLACE(o_gedcom, ' OBJE @$old_xref@', ' OBJE @$new_xref@')"),
1386
                        ]);
1387
                    break;
1388
1389
                default:
1390
                    DB::table('other')
1391
                        ->where('o_file', '=', $tree->id())
1392
                        ->where('o_id', '=', $old_xref)
1393
                        ->where('o_type', '=', $type)
1394
                        ->update([
1395
                            'o_id'     => $new_xref,
1396
                            'o_gedcom' => new Expression("REPLACE(o_gedcom, '0 @$old_xref@ $type', '0 @$new_xref@ $type')"),
1397
                        ]);
1398
1399
                    DB::table('individuals')
1400
                        ->join('link', static function (JoinClause $join): void {
1401
                            $join
1402
                                ->on('l_file', '=', 'i_file')
1403
                                ->on('l_from', '=', 'i_id');
1404
                        })
1405
                        ->where('l_to', '=', $old_xref)
1406
                        ->where('l_type', '=', $type)
1407
                        ->where('i_file', '=', $tree->id())
1408
                        ->update([
1409
                            'i_gedcom' => new Expression("REPLACE(i_gedcom, ' $type @$old_xref@', ' $type @$new_xref@')"),
1410
                        ]);
1411
1412
                    DB::table('families')
1413
                        ->join('link', static function (JoinClause $join): void {
1414
                            $join
1415
                                ->on('l_file', '=', 'f_file')
1416
                                ->on('l_from', '=', 'f_id');
1417
                        })
1418
                        ->where('l_to', '=', $old_xref)
1419
                        ->where('l_type', '=', $type)
1420
                        ->where('f_file', '=', $tree->id())
1421
                        ->update([
1422
                            'f_gedcom' => new Expression("REPLACE(f_gedcom, ' $type @$old_xref@', ' $type @$new_xref@')"),
1423
                        ]);
1424
1425
                    DB::table('media')
1426
                        ->join('link', static function (JoinClause $join): void {
1427
                            $join
1428
                                ->on('l_file', '=', 'm_file')
1429
                                ->on('l_from', '=', 'm_id');
1430
                        })
1431
                        ->where('l_to', '=', $old_xref)
1432
                        ->where('l_type', '=', $type)
1433
                        ->where('m_file', '=', $tree->id())
1434
                        ->update([
1435
                            'm_gedcom' => new Expression("REPLACE(m_gedcom, ' $type @$old_xref@', ' $type @$new_xref@')"),
1436
                        ]);
1437
1438
                    DB::table('sources')
1439
                        ->join('link', static function (JoinClause $join): void {
1440
                            $join
1441
                                ->on('l_file', '=', 's_file')
1442
                                ->on('l_from', '=', 's_id');
1443
                        })
1444
                        ->where('l_to', '=', $old_xref)
1445
                        ->where('l_type', '=', $type)
1446
                        ->where('s_file', '=', $tree->id())
1447
                        ->update([
1448
                            's_gedcom' => new Expression("REPLACE(s_gedcom, ' $type @$old_xref@', ' $type @$new_xref@')"),
1449
                        ]);
1450
1451
                    DB::table('other')
1452
                        ->join('link', static function (JoinClause $join): void {
1453
                            $join
1454
                                ->on('l_file', '=', 'o_file')
1455
                                ->on('l_from', '=', 'o_id');
1456
                        })
1457
                        ->where('l_to', '=', $old_xref)
1458
                        ->where('l_type', '=', $type)
1459
                        ->where('o_file', '=', $tree->id())
1460
                        ->update([
1461
                            'o_gedcom' => new Expression("REPLACE(o_gedcom, ' $type @$old_xref@', ' $type @$new_xref@')"),
1462
                        ]);
1463
                    break;
1464
            }
1465
1466
            DB::table('name')
1467
                ->where('n_file', '=', $tree->id())
1468
                ->where('n_id', '=', $old_xref)
1469
                ->update([
1470
                    'n_id' => $new_xref,
1471
                ]);
1472
1473
            DB::table('default_resn')
1474
                ->where('gedcom_id', '=', $tree->id())
1475
                ->where('xref', '=', $old_xref)
1476
                ->update([
1477
                    'xref' => $new_xref,
1478
                ]);
1479
1480
            DB::table('hit_counter')
1481
                ->where('gedcom_id', '=', $tree->id())
1482
                ->where('page_parameter', '=', $old_xref)
1483
                ->update([
1484
                    'page_parameter' => $new_xref,
1485
                ]);
1486
1487
            DB::table('link')
1488
                ->where('l_file', '=', $tree->id())
1489
                ->where('l_from', '=', $old_xref)
1490
                ->update([
1491
                    'l_from' => $new_xref,
1492
                ]);
1493
1494
            DB::table('link')
1495
                ->where('l_file', '=', $tree->id())
1496
                ->where('l_to', '=', $old_xref)
1497
                ->update([
1498
                    'l_to' => $new_xref,
1499
                ]);
1500
1501
            DB::table('favorite')
1502
                ->where('gedcom_id', '=', $tree->id())
1503
                ->where('xref', '=', $old_xref)
1504
                ->update([
1505
                    'xref' => $new_xref,
1506
                ]);
1507
1508
            unset($xrefs[$old_xref]);
1509
1510
            // How much time do we have left?
1511
            if ($this->timeout_service->isTimeNearlyUp()) {
1512
                FlashMessages::addMessage(I18N::translate('The server’s time limit has been reached.'), 'warning');
1513
                break;
1514
            }
1515
        }
1516
1517
        $url = route('admin-trees-renumber', ['tree' => $tree->name()]);
1518
1519
        return redirect($url);
1520
    }
1521
1522
    /**
1523
     * @param ServerRequestInterface $request
1524
     *
1525
     * @return ResponseInterface
1526
     */
1527
    public function synchronize(ServerRequestInterface $request): ResponseInterface
1528
    {
1529
        $gedcom_files = $this->gedcomFiles();
1530
1531
        foreach ($gedcom_files as $gedcom_file) {
1532
            // Only import files that have changed
1533
            $filemtime = (string) $this->filesystem->getTimestamp($gedcom_file);
1534
1535
            $tree = $this->tree_service->all()->get($gedcom_file) ?? $this->tree_service->create($gedcom_file, $gedcom_file);
1536
1537
            if ($tree->getPreference('filemtime') !== $filemtime) {
1538
                $resource = $this->filesystem->readStream($gedcom_file);
1539
                $stream   = app(StreamFactoryInterface::class)->createStreamFromResource($resource);
1540
                $tree->importGedcomFile($stream, $gedcom_file);
1541
                $stream->close();
1542
                $tree->setPreference('filemtime', $filemtime);
1543
1544
                FlashMessages::addMessage(I18N::translate('The GEDCOM file “%s” has been imported.', e($gedcom_file)), 'success');
1545
            }
1546
        }
1547
1548
        foreach ($this->tree_service->all() as $tree) {
1549
            if (!in_array($tree->name(), $gedcom_files, true)) {
1550
                $this->tree_service->delete($tree);
1551
                FlashMessages::addMessage(I18N::translate('The family tree “%s” has been deleted.', e($tree->title())), 'success');
1552
            }
1553
        }
1554
1555
        return redirect(route('manage-trees'));
1556
    }
1557
1558
    /**
1559
     * @param string $type
1560
     * @param array  $links
1561
     * @param string $xref1
1562
     * @param string $xref2
1563
     * @param string $link
1564
     * @param array  $reciprocal
1565
     *
1566
     * @return bool
1567
     */
1568
    private function checkReverseLink(string $type, array $links, string $xref1, string $xref2, string $link, array $reciprocal): bool
1569
    {
1570
        return $type === $link && (!array_key_exists($xref1, $links[$xref2]) || !in_array($links[$xref2][$xref1], $reciprocal, true));
1571
    }
1572
1573
    /**
1574
     * Create a message linking one record to another.
1575
     *
1576
     * @param Tree   $tree
1577
     * @param string $type1
1578
     * @param string $xref1
1579
     * @param string $type2
1580
     * @param string $xref2
1581
     *
1582
     * @return string
1583
     */
1584
    private function checkLinkMessage(Tree $tree, $type1, $xref1, $type2, $xref2): string
1585
    {
1586
        /* I18N: The placeholders are GEDCOM XREFs and tags. e.g. “INDI I123 contains a FAMC link to F234.” */
1587
        return I18N::translate(
1588
            '%1$s %2$s has a %3$s link to %4$s.',
1589
            $this->formatType($type1),
1590
            $this->checkLink($tree, $xref1),
1591
            $this->formatType($type2),
1592
            $this->checkLink($tree, $xref2)
1593
        );
1594
    }
1595
1596
    /**
1597
     * Format a link to a record.
1598
     *
1599
     * @param Tree   $tree
1600
     * @param string $xref
1601
     *
1602
     * @return string
1603
     */
1604
    private function checkLink(Tree $tree, string $xref): string
1605
    {
1606
        return '<b><a href="' . e(route('record', [
1607
                'xref' => $xref,
1608
                'tree' => $tree->name(),
1609
            ])) . '">' . $xref . '</a></b>';
1610
    }
1611
1612
    /**
1613
     * Format a record type.
1614
     *
1615
     * @param string $type
1616
     *
1617
     * @return string
1618
     */
1619
    private function formatType($type): string
1620
    {
1621
        return '<b title="' . GedcomTag::getLabel($type) . '">' . $type . '</b>';
1622
    }
1623
1624
    /**
1625
     * Find a list of place names that would be updated.
1626
     *
1627
     * @param Tree   $tree
1628
     * @param string $search
1629
     * @param string $replace
1630
     *
1631
     * @return string[]
1632
     */
1633
    private function changePlacesPreview(Tree $tree, string $search, string $replace): array
1634
    {
1635
        // Fetch the latest GEDCOM for each individual and family
1636
        $union = DB::table('families')
1637
            ->where('f_file', '=', $tree->id())
1638
            ->whereContains('f_gedcom', $search)
1639
            ->select(['f_gedcom AS gedcom']);
1640
1641
        return DB::table('individuals')
1642
            ->where('i_file', '=', $tree->id())
1643
            ->whereContains('i_gedcom', $search)
1644
            ->select(['i_gedcom AS gedcom'])
1645
            ->unionAll($union)
1646
            ->pluck('gedcom')
1647
            ->mapWithKeys(static function (string $gedcom) use ($search, $replace): array {
1648
                preg_match_all('/\n2 PLAC ((?:.*, )*)' . preg_quote($search, '/') . '(\n|$)/i', $gedcom, $matches);
1649
1650
                $changes = [];
1651
                foreach ($matches[1] as $prefix) {
1652
                    $changes[$prefix . $search] = $prefix . $replace;
1653
                }
1654
1655
                return $changes;
1656
            })
1657
            ->sort()
1658
            ->all();
1659
    }
1660
1661
    /**
1662
     * Find a list of place names that would be updated.
1663
     *
1664
     * @param Tree   $tree
1665
     * @param string $search
1666
     * @param string $replace
1667
     *
1668
     * @return string[]
1669
     */
1670
    private function changePlacesUpdate(Tree $tree, string $search, string $replace): array
1671
    {
1672
        $individual_changes = DB::table('individuals')
1673
            ->where('i_file', '=', $tree->id())
1674
            ->whereContains('i_gedcom', $search)
1675
            ->select(['individuals.*'])
1676
            ->get()
1677
            ->map(Individual::rowMapper());
1678
1679
        $family_changes = DB::table('families')
1680
            ->where('f_file', '=', $tree->id())
1681
            ->whereContains('f_gedcom', $search)
1682
            ->select(['families.*'])
1683
            ->get()
1684
            ->map(Family::rowMapper());
1685
1686
        return $individual_changes
1687
            ->merge($family_changes)
1688
            ->mapWithKeys(static function (GedcomRecord $record) use ($search, $replace): array {
1689
                $changes = [];
1690
1691
                foreach ($record->facts() as $fact) {
1692
                    $old_place = $fact->attribute('PLAC');
1693
                    if (preg_match('/(^|, )' . preg_quote($search, '/') . '$/i', $old_place)) {
1694
                        $new_place           = preg_replace('/(^|, )' . preg_quote($search, '/') . '$/i', '$1' . $replace, $old_place);
1695
                        $changes[$old_place] = $new_place;
1696
                        $gedcom              = preg_replace('/(\n2 PLAC (?:.*, )*)' . preg_quote($search, '/') . '(\n|$)/i', '$1' . $replace . '$2', $fact->gedcom());
1697
                        $record->updateFact($fact->id(), $gedcom, false);
1698
                    }
1699
                }
1700
1701
                return $changes;
1702
            })
1703
            ->sort()
1704
            ->all();
1705
    }
1706
1707
    /**
1708
     * Count of XREFs used by two trees at the same time.
1709
     *
1710
     * @param Tree $tree1
1711
     * @param Tree $tree2
1712
     *
1713
     * @return int
1714
     */
1715
    private function countCommonXrefs(Tree $tree1, Tree $tree2): int
1716
    {
1717
        $subquery1 = DB::table('individuals')
1718
            ->where('i_file', '=', $tree1->id())
1719
            ->select(['i_id AS xref'])
1720
            ->union(DB::table('families')
1721
                ->where('f_file', '=', $tree1->id())
1722
                ->select(['f_id AS xref']))
1723
            ->union(DB::table('sources')
1724
                ->where('s_file', '=', $tree1->id())
1725
                ->select(['s_id AS xref']))
1726
            ->union(DB::table('media')
1727
                ->where('m_file', '=', $tree1->id())
1728
                ->select(['m_id AS xref']))
1729
            ->union(DB::table('other')
1730
                ->where('o_file', '=', $tree1->id())
1731
                ->whereNotIn('o_type', ['HEAD', 'TRLR'])
1732
                ->select(['o_id AS xref']));
1733
1734
        $subquery2 = DB::table('change')
1735
            ->where('gedcom_id', '=', $tree2->id())
1736
            ->select(['xref AS other_xref'])
1737
            ->union(DB::table('individuals')
1738
                ->where('i_file', '=', $tree2->id())
1739
                ->select(['i_id AS xref']))
1740
            ->union(DB::table('families')
1741
                ->where('f_file', '=', $tree2->id())
1742
                ->select(['f_id AS xref']))
1743
            ->union(DB::table('sources')
1744
                ->where('s_file', '=', $tree2->id())
1745
                ->select(['s_id AS xref']))
1746
            ->union(DB::table('media')
1747
                ->where('m_file', '=', $tree2->id())
1748
                ->select(['m_id AS xref']))
1749
            ->union(DB::table('other')
1750
                ->where('o_file', '=', $tree2->id())
1751
                ->whereNotIn('o_type', ['HEAD', 'TRLR'])
1752
                ->select(['o_id AS xref']));
1753
1754
        return DB::table(new Expression('(' . $subquery1->toSql() . ') AS sub1'))
1755
            ->mergeBindings($subquery1)
1756
            ->joinSub($subquery2, 'sub2', 'other_xref', '=', 'xref')
1757
            ->count();
1758
    }
1759
1760
    /**
1761
     * @param Tree $tree
1762
     *
1763
     * @return array
1764
     */
1765
    private function duplicateRecords(Tree $tree): array
1766
    {
1767
        // We can't do any reasonable checks using MySQL.
1768
        // Will need to wait for a "repositories" table.
1769
        $repositories = [];
1770
1771
        $sources = DB::table('sources')
1772
            ->where('s_file', '=', $tree->id())
1773
            ->groupBy(['s_name'])
1774
            ->having(new Expression('COUNT(s_id)'), '>', '1')
1775
            ->select([new Expression('GROUP_CONCAT(s_id) AS xrefs')])
1776
            ->pluck('xrefs')
1777
            ->map(static function (string $xrefs) use ($tree): array {
1778
                return array_map(static function (string $xref) use ($tree): Source {
1779
                    return Source::getInstance($xref, $tree);
1780
                }, explode(',', $xrefs));
1781
            })
1782
            ->all();
1783
1784
        $individuals = DB::table('dates')
1785
            ->join('name', static function (JoinClause $join): void {
1786
                $join
1787
                    ->on('d_file', '=', 'n_file')
1788
                    ->on('d_gid', '=', 'n_id');
1789
            })
1790
            ->where('d_file', '=', $tree->id())
1791
            ->whereIn('d_fact', ['BIRT', 'CHR', 'BAPM', 'DEAT', 'BURI'])
1792
            ->groupBy(['d_year', 'd_month', 'd_day', 'd_type', 'd_fact', 'n_type', 'n_full'])
1793
            ->having(new Expression('COUNT(DISTINCT d_gid)'), '>', '1')
1794
            ->select([new Expression('GROUP_CONCAT(d_gid) AS xrefs')])
1795
            ->distinct()
1796
            ->pluck('xrefs')
1797
            ->map(static function (string $xrefs) use ($tree): array {
1798
                return array_map(static function (string $xref) use ($tree): Individual {
1799
                    return Individual::getInstance($xref, $tree);
1800
                }, explode(',', $xrefs));
1801
            })
1802
            ->all();
1803
1804
        $families = DB::table('families')
1805
            ->where('f_file', '=', $tree->id())
1806
            ->groupBy([new Expression('LEAST(f_husb, f_wife)')])
1807
            ->groupBy([new Expression('GREATEST(f_husb, f_wife)')])
1808
            ->having(new Expression('COUNT(f_id)'), '>', '1')
1809
            ->select([new Expression('GROUP_CONCAT(f_id) AS xrefs')])
1810
            ->pluck('xrefs')
1811
            ->map(static function (string $xrefs) use ($tree): array {
1812
                return array_map(static function (string $xref) use ($tree): Family {
1813
                    return Family::getInstance($xref, $tree);
1814
                }, explode(',', $xrefs));
1815
            })
1816
            ->all();
1817
1818
        $media = DB::table('media_file')
1819
            ->where('m_file', '=', $tree->id())
1820
            ->where('descriptive_title', '<>', '')
1821
            ->groupBy(['descriptive_title'])
1822
            ->having(new Expression('COUNT(m_id)'), '>', '1')
1823
            ->select([new Expression('GROUP_CONCAT(m_id) AS xrefs')])
1824
            ->pluck('xrefs')
1825
            ->map(static function (string $xrefs) use ($tree): array {
1826
                return array_map(static function (string $xref) use ($tree): Media {
1827
                    return Media::getInstance($xref, $tree);
1828
                }, explode(',', $xrefs));
1829
            })
1830
            ->all();
1831
1832
        return [
1833
            I18N::translate('Repositories')  => $repositories,
1834
            I18N::translate('Sources')       => $sources,
1835
            I18N::translate('Individuals')   => $individuals,
1836
            I18N::translate('Families')      => $families,
1837
            I18N::translate('Media objects') => $media,
1838
        ];
1839
    }
1840
1841
    /**
1842
     * Every XREF used by this tree and also used by some other tree
1843
     *
1844
     * @param Tree $tree
1845
     *
1846
     * @return string[]
1847
     */
1848
    private function duplicateXrefs(Tree $tree): array
1849
    {
1850
        $subquery1 = DB::table('individuals')
1851
            ->where('i_file', '=', $tree->id())
1852
            ->select(['i_id AS xref', new Expression("'INDI' AS type")])
1853
            ->union(DB::table('families')
1854
                ->where('f_file', '=', $tree->id())
1855
                ->select(['f_id AS xref', new Expression("'FAM' AS type")]))
1856
            ->union(DB::table('sources')
1857
                ->where('s_file', '=', $tree->id())
1858
                ->select(['s_id AS xref', new Expression("'SOUR' AS type")]))
1859
            ->union(DB::table('media')
1860
                ->where('m_file', '=', $tree->id())
1861
                ->select(['m_id AS xref', new Expression("'OBJE' AS type")]))
1862
            ->union(DB::table('other')
1863
                ->where('o_file', '=', $tree->id())
1864
                ->whereNotIn('o_type', ['HEAD', 'TRLR'])
1865
                ->select(['o_id AS xref', 'o_type AS type']));
1866
1867
        $subquery2 = DB::table('change')
1868
            ->where('gedcom_id', '<>', $tree->id())
1869
            ->select(['xref AS other_xref'])
1870
            ->union(DB::table('individuals')
1871
                ->where('i_file', '<>', $tree->id())
1872
                ->select(['i_id AS xref']))
1873
            ->union(DB::table('families')
1874
                ->where('f_file', '<>', $tree->id())
1875
                ->select(['f_id AS xref']))
1876
            ->union(DB::table('sources')
1877
                ->where('s_file', '<>', $tree->id())
1878
                ->select(['s_id AS xref']))
1879
            ->union(DB::table('media')
1880
                ->where('m_file', '<>', $tree->id())
1881
                ->select(['m_id AS xref']))
1882
            ->union(DB::table('other')
1883
                ->where('o_file', '<>', $tree->id())
1884
                ->whereNotIn('o_type', ['HEAD', 'TRLR'])
1885
                ->select(['o_id AS xref']));
1886
1887
        return DB::table(new Expression('(' . $subquery1->toSql() . ') AS sub1'))
1888
            ->mergeBindings($subquery1)
1889
            ->joinSub($subquery2, 'sub2', 'other_xref', '=', 'xref')
1890
            ->pluck('type', 'xref')
1891
            ->all();
1892
    }
1893
1894
    /**
1895
     * A list of GEDCOM files in the data folder.
1896
     *
1897
     * @return array
1898
     */
1899
    private function gedcomFiles(): array
1900
    {
1901
        return Collection::make($this->filesystem->listContents())
1902
            ->filter(function (array $path): bool {
1903
                if ($path['type'] !== 'file') {
1904
                    return false;
1905
                }
1906
1907
                $stream = $this->filesystem->readStream($path['path']);
1908
                $header = fread($stream, 64);
1909
                fclose($stream);
1910
1911
                return preg_match('/^(' . Gedcom::UTF8_BOM . ')?0 *HEAD/', $header) > 0;
1912
            })
1913
            ->map(static function (array $path): string {
1914
                return $path['path'];
1915
            })
1916
            ->sort()
1917
            ->all();
1918
    }
1919
1920
    /**
1921
     * @return Collection
1922
     */
1923
    private function themeOptions(): Collection
1924
    {
1925
        return $this->module_service
1926
            ->findByInterface(ModuleThemeInterface::class)
1927
            ->map($this->module_service->titleMapper())
1928
            ->prepend(I18N::translate('<default theme>'), '');
1929
    }
1930
}
1931