Passed
Push — master ( e25212...5afbc5 )
by Greg
05:14
created

AdminTreesController::duplicates()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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