Passed
Push — master ( da7f6d...6d5769 )
by Greg
05:57
created

AdminTreesController::export()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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