Passed
Push — master ( cd50a3...da1c67 )
by Greg
05:21
created

AdminTreesController::setDefault()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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