Passed
Push — master ( 247434...35b016 )
by Greg
07:11
created

AdminTreesController::preferences()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 125
Code Lines 84

Duplication

Lines 0
Ratio 0 %

Importance

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