Passed
Push — master ( aabcb6...83d280 )
by Greg
05:15
created

AdminTreesController::checkReverseLink()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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