Passed
Push — master ( 64c83a...f96f51 )
by Greg
09:17
created

FunctionsImport::updateNames()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 40
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
eloc 29
c 2
b 0
f 0
nc 5
nop 3
dl 0
loc 40
rs 9.456
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2020 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Functions;
21
22
use Fisharebest\Webtrees\Date;
23
use Fisharebest\Webtrees\Exceptions\GedcomErrorException;
24
use Fisharebest\Webtrees\Factory;
25
use Fisharebest\Webtrees\Family;
26
use Fisharebest\Webtrees\Gedcom;
27
use Fisharebest\Webtrees\GedcomTag;
28
use Fisharebest\Webtrees\Header;
29
use Fisharebest\Webtrees\Individual;
30
use Fisharebest\Webtrees\Location;
31
use Fisharebest\Webtrees\Media;
32
use Fisharebest\Webtrees\Note;
33
use Fisharebest\Webtrees\Place;
34
use Fisharebest\Webtrees\PlaceLocation;
35
use Fisharebest\Webtrees\Repository;
36
use Fisharebest\Webtrees\Services\GedcomService;
37
use Fisharebest\Webtrees\Soundex;
38
use Fisharebest\Webtrees\Source;
39
use Fisharebest\Webtrees\Submission;
40
use Fisharebest\Webtrees\Submitter;
41
use Fisharebest\Webtrees\Tree;
42
use Illuminate\Database\Capsule\Manager as DB;
43
use Illuminate\Database\Query\JoinClause;
44
45
use function array_intersect_key;
46
use function array_map;
47
use function array_unique;
48
use function date;
49
use function preg_match;
50
use function preg_match_all;
51
use function str_contains;
52
use function str_starts_with;
53
use function strtoupper;
54
use function trim;
55
56
use const PREG_SET_ORDER;
57
58
/**
59
 * Class FunctionsImport - common functions
60
 */
61
class FunctionsImport
62
{
63
    /**
64
     * Tidy up a gedcom record on import, so that we can access it consistently/efficiently.
65
     *
66
     * @param string $rec
67
     * @param Tree   $tree
68
     *
69
     * @return string
70
     */
71
    public static function reformatRecord(string $rec, Tree $tree): string
72
    {
73
        $gedcom_service = app(GedcomService::class);
74
        assert($gedcom_service instanceof GedcomService);
75
76
        // Strip out mac/msdos line endings
77
        $rec = preg_replace("/[\r\n]+/", "\n", $rec);
78
79
        // Extract lines from the record; lines consist of: level + optional xref + tag + optional data
80
        $num_matches = preg_match_all('/^[ \t]*(\d+)[ \t]*(@[^@]*@)?[ \t]*(\w+)[ \t]?(.*)$/m', $rec, $matches, PREG_SET_ORDER);
81
82
        // Process the record line-by-line
83
        $newrec = '';
84
        foreach ($matches as $n => $match) {
85
            [, $level, $xref, $tag, $data] = $match;
86
87
            $tag = $gedcom_service->canonicalTag($tag);
88
89
            switch ($tag) {
90
                case 'AFN':
91
                    // AFN values are upper case
92
                    $data = strtoupper($data);
93
                    break;
94
                case 'DATE':
95
                    // Preserve text from INT dates
96
                    if (str_contains($data, '(')) {
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

96
                    if (/** @scrutinizer ignore-deprecated */ str_contains($data, '(')) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
97
                        [$date, $text] = explode('(', $data, 2);
98
                        $text = ' (' . $text;
99
                    } else {
100
                        $date = $data;
101
                        $text = '';
102
                    }
103
                    // Capitals
104
                    $date = strtoupper($date);
105
                    // Temporarily add leading/trailing spaces, to allow efficient matching below
106
                    $date = " {$date} ";
107
                    // Ensure space digits and letters
108
                    $date = preg_replace('/([A-Z])(\d)/', '$1 $2', $date);
109
                    $date = preg_replace('/(\d)([A-Z])/', '$1 $2', $date);
110
                    // Ensure space before/after calendar escapes
111
                    $date = preg_replace('/@#[^@]+@/', ' $0 ', $date);
112
                    // "BET." => "BET"
113
                    $date = preg_replace('/(\w\w)\./', '$1', $date);
114
                    // "CIR" => "ABT"
115
                    $date = str_replace(' CIR ', ' ABT ', $date);
116
                    $date = str_replace(' APX ', ' ABT ', $date);
117
                    // B.C. => BC (temporarily, to allow easier handling of ".")
118
                    $date = str_replace(' B.C. ', ' BC ', $date);
119
                    // TMG uses "EITHER X OR Y"
120
                    $date = preg_replace('/^ EITHER (.+) OR (.+)/', ' BET $1 AND $2', $date);
121
                    // "BET X - Y " => "BET X AND Y"
122
                    $date = preg_replace('/^(.* BET .+) - (.+)/', '$1 AND $2', $date);
123
                    $date = preg_replace('/^(.* FROM .+) - (.+)/', '$1 TO $2', $date);
124
                    // "@#ESC@ FROM X TO Y" => "FROM @#ESC@ X TO @#ESC@ Y"
125
                    $date = preg_replace('/^ +(@#[^@]+@) +FROM +(.+) +TO +(.+)/', ' FROM $1 $2 TO $1 $3', $date);
126
                    $date = preg_replace('/^ +(@#[^@]+@) +BET +(.+) +AND +(.+)/', ' BET $1 $2 AND $1 $3', $date);
127
                    // "@#ESC@ AFT X" => "AFT @#ESC@ X"
128
                    $date = preg_replace('/^ +(@#[^@]+@) +(FROM|BET|TO|AND|BEF|AFT|CAL|EST|INT|ABT) +(.+)/', ' $2 $1 $3', $date);
129
                    // Ignore any remaining punctuation, e.g. "14-MAY, 1900" => "14 MAY 1900"
130
                    // (don't change "/" - it is used in NS/OS dates)
131
                    $date = preg_replace('/[.,:;-]/', ' ', $date);
132
                    // BC => B.C.
133
                    $date = str_replace(' BC ', ' B.C. ', $date);
134
                    // Append the "INT" text
135
                    $data = $date . $text;
136
                    break;
137
                case '_FILE':
138
                    $tag = 'FILE';
139
                    break;
140
                case 'FORM':
141
                    // Consistent commas
142
                    $data = preg_replace('/ *, */', ', ', $data);
143
                    break;
144
                case 'HEAD':
145
                    // HEAD records don't have an XREF or DATA
146
                    if ($level === '0') {
147
                        $xref = '';
148
                        $data = '';
149
                    }
150
                    break;
151
                case 'NAME':
152
                    // Tidy up non-printing characters
153
                    $data = preg_replace('/  +/', ' ', trim($data));
154
                    break;
155
                case 'PEDI':
156
                    // PEDI values are lower case
157
                    $data = strtolower($data);
158
                    break;
159
                case 'PLAC':
160
                    // Consistent commas
161
                    $data = preg_replace('/ *[,,،] */u', ', ', $data);
162
                    // The Master Genealogist stores LAT/LONG data in the PLAC field, e.g. Pennsylvania, USA, 395945N0751013W
163
                    if (preg_match('/(.*), (\d\d)(\d\d)(\d\d)([NS])(\d\d\d)(\d\d)(\d\d)([EW])$/', $data, $match)) {
164
                        $data =
165
                            $match[1] . "\n" .
166
                            ($level + 1) . " MAP\n" .
167
                            ($level + 2) . ' LATI ' . ($match[5] . round($match[2] + ($match[3] / 60) + ($match[4] / 3600), 4)) . "\n" .
168
                            ($level + 2) . ' LONG ' . ($match[9] . round($match[6] + ($match[7] / 60) + ($match[8] / 3600), 4));
169
                    }
170
                    break;
171
                case 'RESN':
172
                    // RESN values are lower case (confidential, privacy, locked, none)
173
                    $data = strtolower($data);
174
                    if ($data === 'invisible') {
175
                        $data = 'confidential'; // From old versions of Legacy.
176
                    }
177
                    break;
178
                case 'SEX':
179
                    $data = strtoupper($data);
180
                    break;
181
                case 'STAT':
182
                    if ($data === 'CANCELLED') {
183
                        // PhpGedView mis-spells this tag - correct it.
184
                        $data = 'CANCELED';
185
                    }
186
                    break;
187
                case 'TEMP':
188
                    // Temple codes are upper case
189
                    $data = strtoupper($data);
190
                    break;
191
                case 'TRLR':
192
                    // TRLR records don't have an XREF or DATA
193
                    if ($level === '0') {
194
                        $xref = '';
195
                        $data = '';
196
                    }
197
                    break;
198
            }
199
            // Suppress "Y", for facts/events with a DATE or PLAC
200
            if ($data === 'y') {
201
                $data = 'Y';
202
            }
203
            if ($level === '1' && $data === 'Y') {
204
                for ($i = $n + 1; $i < $num_matches - 1 && $matches[$i][1] !== '1'; ++$i) {
205
                    if ($matches[$i][3] === 'DATE' || $matches[$i][3] === 'PLAC') {
206
                        $data = '';
207
                        break;
208
                    }
209
                }
210
            }
211
            // Reassemble components back into a single line
212
            switch ($tag) {
213
                default:
214
                    // Remove tabs and multiple/leading/trailing spaces
215
                    $data = strtr($data, ["\t" => ' ']);
216
                    $data = trim($data, ' ');
217
                    while (str_contains($data, '  ')) {
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

217
                    while (/** @scrutinizer ignore-deprecated */ str_contains($data, '  ')) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
218
                        $data = strtr($data, ['  ' => ' ']);
219
                    }
220
                    $newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level === '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag !== 'NOTE' ? '' : ' ' . $data);
221
                    break;
222
                case 'NOTE':
223
                case 'TEXT':
224
                case 'DATA':
225
                case 'CONT':
226
                    $newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level === '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag !== 'NOTE' ? '' : ' ' . $data);
227
                    break;
228
                case 'FILE':
229
                    // Strip off the user-defined path prefix
230
                    $GEDCOM_MEDIA_PATH = $tree->getPreference('GEDCOM_MEDIA_PATH');
231
                    if ($GEDCOM_MEDIA_PATH !== '' && str_starts_with($data, $GEDCOM_MEDIA_PATH)) {
232
                        $data = substr($data, strlen($GEDCOM_MEDIA_PATH));
233
                    }
234
                    // convert backslashes in filenames to forward slashes
235
                    $data = preg_replace("/\\\\/", '/', $data);
236
237
                    $newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level === '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag !== 'NOTE' ? '' : ' ' . $data);
238
                    break;
239
                case 'CONC':
240
                    // Merge CONC lines, to simplify access later on.
241
                    $newrec .= ($tree->getPreference('WORD_WRAPPED_NOTES') ? ' ' : '') . $data;
242
                    break;
243
            }
244
        }
245
246
        return $newrec;
247
    }
248
249
    /**
250
     * import record into database
251
     * this function will parse the given gedcom record and add it to the database
252
     *
253
     * @param string $gedrec the raw gedcom record to parse
254
     * @param Tree   $tree   import the record into this tree
255
     * @param bool   $update whether or not this is an updated record that has been accepted
256
     *
257
     * @return void
258
     * @throws GedcomErrorException
259
     */
260
    public static function importRecord(string $gedrec, Tree $tree, bool $update): void
261
    {
262
        $tree_id = $tree->id();
263
264
        // Escaped @ signs (only if importing from file)
265
        if (!$update) {
266
            $gedrec = str_replace('@@', '@', $gedrec);
267
        }
268
269
        // Standardise gedcom format
270
        $gedrec = self::reformatRecord($gedrec, $tree);
271
272
        // import different types of records
273
        if (preg_match('/^0 @(' . Gedcom::REGEX_XREF . ')@ (' . Gedcom::REGEX_TAG . ')/', $gedrec, $match)) {
274
            [, $xref, $type] = $match;
275
            // check for a _UID, if the record doesn't have one, add one
276
            if ($tree->getPreference('GENERATE_UIDS') === '1' && !str_contains($gedrec, "\n1 _UID ")) {
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

276
            if ($tree->getPreference('GENERATE_UIDS') === '1' && !/** @scrutinizer ignore-deprecated */ str_contains($gedrec, "\n1 _UID ")) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
277
                $gedrec .= "\n1 _UID " . GedcomTag::createUid();
278
            }
279
        } elseif (preg_match('/0 (HEAD|TRLR|_PLAC |_PLAC_DEFN)/', $gedrec, $match)) {
280
            $type = $match[1];
281
            $xref = $type; // For records without an XREF, use the type as a pseudo XREF.
282
        } else {
283
            throw new GedcomErrorException($gedrec);
284
        }
285
286
        // If the user has downloaded their GEDCOM data (containing media objects) and edited it
287
        // using an application which does not support (and deletes) media objects, then add them
288
        // back in.
289
        if ($tree->getPreference('keep_media')) {
290
            $old_linked_media = DB::table('link')
291
                ->where('l_from', '=', $xref)
292
                ->where('l_file', '=', $tree_id)
293
                ->where('l_type', '=', 'OBJE')
294
                ->pluck('l_to');
295
296
            // Delete these links - so that we do not insert them again in updateLinks()
297
            DB::table('link')
298
                ->where('l_from', '=', $xref)
299
                ->where('l_file', '=', $tree_id)
300
                ->where('l_type', '=', 'OBJE')
301
                ->delete();
302
303
            foreach ($old_linked_media as $media_id) {
304
                $gedrec .= "\n1 OBJE @" . $media_id . '@';
305
            }
306
        }
307
308
        // Convert inline media into media objects
309
        $gedrec = self::convertInlineMedia($tree, $gedrec);
310
311
        switch ($type) {
312
            case Individual::RECORD_TYPE:
313
                $record = Factory::individual()->new($xref, $gedrec, null, $tree);
314
315
                if (preg_match('/\n1 RIN (.+)/', $gedrec, $match)) {
316
                    $rin = $match[1];
317
                } else {
318
                    $rin = $xref;
319
                }
320
321
                DB::table('individuals')->insert([
322
                    'i_id'     => $xref,
323
                    'i_file'   => $tree_id,
324
                    'i_rin'    => $rin,
325
                    'i_sex'    => $record->sex(),
326
                    'i_gedcom' => $gedrec,
327
                ]);
328
329
                // Update the cross-reference/index tables.
330
                self::updatePlaces($xref, $tree, $gedrec);
331
                self::updateDates($xref, $tree_id, $gedrec);
332
                self::updateNames($xref, $tree_id, $record);
333
                break;
334
335
            case Family::RECORD_TYPE:
336
                if (preg_match('/\n1 HUSB @(' . Gedcom::REGEX_XREF . ')@/', $gedrec, $match)) {
337
                    $husb = $match[1];
338
                } else {
339
                    $husb = '';
340
                }
341
                if (preg_match('/\n1 WIFE @(' . Gedcom::REGEX_XREF . ')@/', $gedrec, $match)) {
342
                    $wife = $match[1];
343
                } else {
344
                    $wife = '';
345
                }
346
                $nchi = preg_match_all('/\n1 CHIL @(' . Gedcom::REGEX_XREF . ')@/', $gedrec, $match);
347
                if (preg_match('/\n1 NCHI (\d+)/', $gedrec, $match)) {
348
                    $nchi = max($nchi, $match[1]);
349
                }
350
351
                DB::table('families')->insert([
352
                    'f_id'      => $xref,
353
                    'f_file'    => $tree_id,
354
                    'f_husb'    => $husb,
355
                    'f_wife'    => $wife,
356
                    'f_gedcom'  => $gedrec,
357
                    'f_numchil' => $nchi,
358
                ]);
359
360
                // Update the cross-reference/index tables.
361
                self::updatePlaces($xref, $tree, $gedrec);
362
                self::updateDates($xref, $tree_id, $gedrec);
363
                break;
364
365
            case Source::RECORD_TYPE:
366
                if (preg_match('/\n1 TITL (.+)/', $gedrec, $match)) {
367
                    $name = $match[1];
368
                } elseif (preg_match('/\n1 ABBR (.+)/', $gedrec, $match)) {
369
                    $name = $match[1];
370
                } else {
371
                    $name = $xref;
372
                }
373
374
                DB::table('sources')->insert([
375
                    's_id'     => $xref,
376
                    's_file'   => $tree_id,
377
                    's_name'   => mb_substr($name, 0, 255),
378
                    's_gedcom' => $gedrec,
379
                ]);
380
                break;
381
382
            case Repository::RECORD_TYPE:
383
            case Note::RECORD_TYPE:
384
            case Submission::RECORD_TYPE:
385
            case Submitter::RECORD_TYPE:
386
            case Location::RECORD_TYPE:
387
                DB::table('other')->insert([
388
                    'o_id'     => $xref,
389
                    'o_file'   => $tree_id,
390
                    'o_type'   => $type,
391
                    'o_gedcom' => $gedrec,
392
                ]);
393
                break;
394
395
            case Header::RECORD_TYPE:
396
                // Force HEAD records to have a creation date.
397
                if (!str_contains($gedrec, "\n1 DATE ")) {
0 ignored issues
show
Deprecated Code introduced by
The function str_contains() has been deprecated: Str::contains() should be used directly instead. Will be removed in Laravel 6.0. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

397
                if (!/** @scrutinizer ignore-deprecated */ str_contains($gedrec, "\n1 DATE ")) {

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
398
                    $today = strtoupper(date('d M Y'));
399
                    $gedrec .= "\n1 DATE " . $today;
400
                }
401
402
                DB::table('other')->insert([
403
                    'o_id'     => $xref,
404
                    'o_file'   => $tree_id,
405
                    'o_type'   => Header::RECORD_TYPE,
406
                    'o_gedcom' => $gedrec,
407
                ]);
408
                break;
409
410
411
            case Media::RECORD_TYPE:
412
                $record = Factory::media()->new($xref, $gedrec, null, $tree);
413
414
                DB::table('media')->insert([
415
                    'm_id'     => $xref,
416
                    'm_file'   => $tree_id,
417
                    'm_gedcom' => $gedrec,
418
                ]);
419
420
                foreach ($record->mediaFiles() as $media_file) {
421
                    DB::table('media_file')->insert([
422
                        'm_id'                 => $xref,
423
                        'm_file'               => $tree_id,
424
                        'multimedia_file_refn' => mb_substr($media_file->filename(), 0, 248),
425
                        'multimedia_format'    => mb_substr($media_file->format(), 0, 4),
426
                        'source_media_type'    => mb_substr($media_file->type(), 0, 15),
427
                        'descriptive_title'    => mb_substr($media_file->title(), 0, 248),
428
                    ]);
429
                }
430
                break;
431
432
            case '_PLAC ':
433
                self::importTNGPlac($gedrec);
434
                return;
435
436
            case '_PLAC_DEFN':
437
                self::importLegacyPlacDefn($gedrec);
438
                return;
439
440
            default: // Custom record types.
441
                DB::table('other')->insert([
442
                    'o_id'     => $xref,
443
                    'o_file'   => $tree_id,
444
                    'o_type'   => mb_substr($type, 0, 15),
445
                    'o_gedcom' => $gedrec,
446
                ]);
447
                break;
448
        }
449
450
        // Update the cross-reference/index tables.
451
        self::updateLinks($xref, $tree_id, $gedrec);
452
    }
453
454
    /**
455
     * Legacy Family Tree software generates _PLAC_DEFN records containing LAT/LONG values
456
     *
457
     * @param string $gedcom
458
     */
459
    private static function importLegacyPlacDefn(string $gedcom): void
460
    {
461
        $gedcom_service = new GedcomService();
462
463
        if (preg_match('/\n1 PLAC (.+)/', $gedcom, $match)) {
464
            $place_name = $match[1];
465
        } else {
466
            return;
467
        }
468
469
        if (preg_match('/\n3 LATI ([NS].+)/', $gedcom, $match)) {
470
            $latitude = $gedcom_service->readLatitude($match[1]);
471
        } else {
472
            return;
473
        }
474
475
        if (preg_match('/\n3 LONG ([EW].+)/', $gedcom, $match)) {
476
            $longitude = $gedcom_service->readLongitude($match[1]);
477
        } else {
478
            return;
479
        }
480
481
        $location = new PlaceLocation($place_name);
482
483
        if ($location->latitude() === 0.0 && $location->longitude() === 0.0) {
0 ignored issues
show
introduced by
The condition $location->latitude() === 0.0 is always false.
Loading history...
484
            DB::table('placelocation')
485
                ->where('pl_id', '=', $location->id())
486
                ->update([
487
                    'pl_lati' => $latitude,
488
                    'pl_long' => $longitude,
489
                ]);
490
        }
491
    }
492
493
    /**
494
     * Legacy Family Tree software generates _PLAC_DEFN records containing LAT/LONG values
495
     *
496
     * @param string $gedcom
497
     */
498
    private static function importTNGPlac(string $gedcom): void
499
    {
500
        $gedcom_service = new GedcomService();
0 ignored issues
show
Unused Code introduced by
The assignment to $gedcom_service is dead and can be removed.
Loading history...
501
502
        if (preg_match('/^0 _PLAC (.+)/', $gedcom, $match)) {
503
            $place_name = $match[1];
504
        } else {
505
            return;
506
        }
507
508
        if (preg_match('/\n2 LATI (.+)/', $gedcom, $match)) {
509
            $latitude = (float) $match[1];
510
        } else {
511
            return;
512
        }
513
514
        if (preg_match('/\n2 LONG (.+)/', $gedcom, $match)) {
515
            $longitude = (float) $match[1];
516
        } else {
517
            return;
518
        }
519
520
        $location = new PlaceLocation($place_name);
521
522
        if ($location->latitude() === 0.0 && $location->longitude() === 0.0) {
0 ignored issues
show
introduced by
The condition $location->latitude() === 0.0 is always false.
Loading history...
523
            DB::table('placelocation')
524
                ->where('pl_id', '=', $location->id())
525
                ->update([
526
                    'pl_lati' => $latitude,
527
                    'pl_long' => $longitude,
528
                ]);
529
        }
530
    }
531
532
    /**
533
     * Extract all level 2 places from the given record and insert them into the places table
534
     *
535
     * @param string $xref
536
     * @param Tree   $tree
537
     * @param string $gedrec
538
     *
539
     * @return void
540
     */
541
    public static function updatePlaces(string $xref, Tree $tree, string $gedrec): void
542
    {
543
        // Insert all new rows together
544
        $rows = [];
545
546
        preg_match_all('/^[2-9] PLAC (.+)/m', $gedrec, $matches);
547
548
        $places = array_unique($matches[1]);
549
550
        foreach ($places as $place_name) {
551
            $place = new Place($place_name, $tree);
552
553
            // Calling Place::id() will create the entry in the database, if it doesn't already exist.
554
            while ($place->id() !== 0) {
555
                $rows[] = [
556
                    'pl_p_id' => $place->id(),
557
                    'pl_gid'  => $xref,
558
                    'pl_file' => $tree->id(),
559
                ];
560
561
                $place = $place->parent();
562
            }
563
        }
564
565
        // array_unique doesn't work with arrays of arrays
566
        $rows = array_intersect_key($rows, array_unique(array_map('serialize', $rows)));
567
568
        DB::table('placelinks')->insert($rows);
569
    }
570
571
    /**
572
     * Extract all the dates from the given record and insert them into the database.
573
     *
574
     * @param string $xref
575
     * @param int    $ged_id
576
     * @param string $gedrec
577
     *
578
     * @return void
579
     */
580
    public static function updateDates(string $xref, int $ged_id, string $gedrec): void
581
    {
582
        // Insert all new rows together
583
        $rows = [];
584
585
        preg_match_all("/\n1 (\w+).*(?:\n[2-9].*)*(?:\n2 DATE (.+))(?:\n[2-9].*)*/", $gedrec, $matches, PREG_SET_ORDER);
586
587
        foreach ($matches as $match) {
588
            $fact = $match[1];
589
            $date = new Date($match[2]);
590
            $rows[] = [
591
                'd_day'        => $date->minimumDate()->day,
592
                'd_month'      => $date->minimumDate()->format('%O'),
593
                'd_mon'        => $date->minimumDate()->month,
594
                'd_year'       => $date->minimumDate()->year,
595
                'd_julianday1' => $date->minimumDate()->minimumJulianDay(),
596
                'd_julianday2' => $date->minimumDate()->maximumJulianDay(),
597
                'd_fact'       => $fact,
598
                'd_gid'        => $xref,
599
                'd_file'       => $ged_id,
600
                'd_type'       => $date->minimumDate()->format('%@'),
601
            ];
602
603
            $rows[] = [
604
                'd_day'        => $date->maximumDate()->day,
605
                'd_month'      => $date->maximumDate()->format('%O'),
606
                'd_mon'        => $date->maximumDate()->month,
607
                'd_year'       => $date->maximumDate()->year,
608
                'd_julianday1' => $date->maximumDate()->minimumJulianDay(),
609
                'd_julianday2' => $date->maximumDate()->maximumJulianDay(),
610
                'd_fact'       => $fact,
611
                'd_gid'        => $xref,
612
                'd_file'       => $ged_id,
613
                'd_type'       => $date->minimumDate()->format('%@'),
614
            ];
615
        }
616
617
        // array_unique doesn't work with arrays of arrays
618
        $rows = array_intersect_key($rows, array_unique(array_map('serialize', $rows)));
619
620
        DB::table('dates')->insert($rows);
621
    }
622
623
    /**
624
     * Extract all the links from the given record and insert them into the database
625
     *
626
     * @param string $xref
627
     * @param int    $ged_id
628
     * @param string $gedrec
629
     *
630
     * @return void
631
     */
632
    public static function updateLinks(string $xref, int $ged_id, string $gedrec): void
633
    {
634
        // Insert all new rows together
635
        $rows = [];
636
637
        preg_match_all('/^\d+ (' . Gedcom::REGEX_TAG . ') @(' . Gedcom::REGEX_XREF . ')@/m', $gedrec, $matches, PREG_SET_ORDER);
638
639
        foreach ($matches as $match) {
640
            // Take care of "duplicates" that differ on case/collation, e.g. "SOUR @S1@" and "SOUR @s1@"
641
            $rows[$match[1] . strtoupper($match[2])] = [
642
                'l_from' => $xref,
643
                'l_to'   => $match[2],
644
                'l_type' => $match[1],
645
                'l_file' => $ged_id,
646
            ];
647
        }
648
649
        DB::table('link')->insert($rows);
650
    }
651
652
    /**
653
     * Extract all the names from the given record and insert them into the database.
654
     *
655
     * @param string     $xref
656
     * @param int        $ged_id
657
     * @param Individual $record
658
     *
659
     * @return void
660
     */
661
    public static function updateNames(string $xref, int $ged_id, Individual $record): void
662
    {
663
        // Insert all new rows together
664
        $rows = [];
665
666
        foreach ($record->getAllNames() as $n => $name) {
667
            if ($name['givn'] === '@P.N.') {
668
                $soundex_givn_std = null;
669
                $soundex_givn_dm  = null;
670
            } else {
671
                $soundex_givn_std = Soundex::russell($name['givn']);
672
                $soundex_givn_dm  = Soundex::daitchMokotoff($name['givn']);
673
            }
674
675
            if ($name['surn'] === '@N.N.') {
676
                $soundex_surn_std = null;
677
                $soundex_surn_dm  = null;
678
            } else {
679
                $soundex_surn_std = Soundex::russell($name['surname']);
680
                $soundex_surn_dm  = Soundex::daitchMokotoff($name['surname']);
681
            }
682
683
            $rows[] = [
684
                'n_file'             => $ged_id,
685
                'n_id'               => $xref,
686
                'n_num'              => $n,
687
                'n_type'             => $name['type'],
688
                'n_sort'             => mb_substr($name['sort'], 0, 255),
689
                'n_full'             => mb_substr($name['fullNN'], 0, 255),
690
                'n_surname'          => mb_substr($name['surname'], 0, 255),
691
                'n_surn'             => mb_substr($name['surn'], 0, 255),
692
                'n_givn'             => mb_substr($name['givn'], 0, 255),
693
                'n_soundex_givn_std' => $soundex_givn_std,
694
                'n_soundex_surn_std' => $soundex_surn_std,
695
                'n_soundex_givn_dm'  => $soundex_givn_dm,
696
                'n_soundex_surn_dm'  => $soundex_surn_dm,
697
            ];
698
        }
699
700
        DB::table('name')->insert($rows);
701
    }
702
703
    /**
704
     * Extract inline media data, and convert to media objects.
705
     *
706
     * @param Tree   $tree
707
     * @param string $gedrec
708
     *
709
     * @return string
710
     */
711
    public static function convertInlineMedia(Tree $tree, string $gedrec): string
712
    {
713
        while (preg_match('/\n1 OBJE(?:\n[2-9].+)+/', $gedrec, $match)) {
714
            $gedrec = str_replace($match[0], self::createMediaObject(1, $match[0], $tree), $gedrec);
715
        }
716
        while (preg_match('/\n2 OBJE(?:\n[3-9].+)+/', $gedrec, $match)) {
717
            $gedrec = str_replace($match[0], self::createMediaObject(2, $match[0], $tree), $gedrec);
718
        }
719
        while (preg_match('/\n3 OBJE(?:\n[4-9].+)+/', $gedrec, $match)) {
720
            $gedrec = str_replace($match[0], self::createMediaObject(3, $match[0], $tree), $gedrec);
721
        }
722
723
        return $gedrec;
724
    }
725
726
    /**
727
     * Create a new media object, from inline media data.
728
     *
729
     * @param int    $level
730
     * @param string $gedrec
731
     * @param Tree   $tree
732
     *
733
     * @return string
734
     */
735
    public static function createMediaObject(int $level, string $gedrec, Tree $tree): string
736
    {
737
        if (preg_match('/\n\d FILE (.+)/', $gedrec, $file_match)) {
738
            $file = $file_match[1];
739
        } else {
740
            $file = '';
741
        }
742
743
        if (preg_match('/\n\d TITL (.+)/', $gedrec, $file_match)) {
744
            $titl = $file_match[1];
745
        } else {
746
            $titl = '';
747
        }
748
749
        // Have we already created a media object with the same title/filename?
750
        $xref = DB::table('media_file')
751
            ->where('m_file', '=', $tree->id())
752
            ->where('descriptive_title', '=', $titl)
753
            ->where('multimedia_file_refn', '=', mb_substr($file, 0, 248))
754
            ->value('m_id');
755
756
        if ($xref === null) {
757
            $xref = Factory::xref()->make(Media::RECORD_TYPE);
758
            // renumber the lines
759
            $gedrec = preg_replace_callback('/\n(\d+)/', static function (array $m) use ($level): string {
760
                return "\n" . ($m[1] - $level);
761
            }, $gedrec);
762
            // convert to an object
763
            $gedrec = str_replace("\n0 OBJE\n", '0 @' . $xref . "@ OBJE\n", $gedrec);
764
765
            // Fix Legacy GEDCOMS
766
            $gedrec = preg_replace('/\n1 FORM (.+)\n1 FILE (.+)\n1 TITL (.+)/', "\n1 FILE $2\n2 FORM $1\n2 TITL $3", $gedrec);
767
768
            // Fix FTB GEDCOMS
769
            $gedrec = preg_replace('/\n1 FORM (.+)\n1 TITL (.+)\n1 FILE (.+)/', "\n1 FILE $3\n2 FORM $1\n2 TITL $2", $gedrec);
770
771
            // Fix RM7 GEDCOMS
772
            $gedrec = preg_replace('/\n1 FILE (.+)\n1 FORM (.+)\n1 TITL (.+)/', "\n1 FILE $1\n2 FORM $2\n2 TITL $3", $gedrec);
773
774
            // Create new record
775
            $record = Factory::media()->new($xref, $gedrec, null, $tree);
776
777
            DB::table('media')->insert([
778
                'm_id'     => $xref,
779
                'm_file'   => $tree->id(),
780
                'm_gedcom' => $gedrec,
781
            ]);
782
783
            foreach ($record->mediaFiles() as $media_file) {
784
                DB::table('media_file')->insert([
785
                    'm_id'                 => $xref,
786
                    'm_file'               => $tree->id(),
787
                    'multimedia_file_refn' => mb_substr($media_file->filename(), 0, 248),
788
                    'multimedia_format'    => mb_substr($media_file->format(), 0, 4),
789
                    'source_media_type'    => mb_substr($media_file->type(), 0, 15),
790
                    'descriptive_title'    => mb_substr($media_file->title(), 0, 248),
791
                ]);
792
            }
793
        }
794
795
        return "\n" . $level . ' OBJE @' . $xref . '@';
796
    }
797
798
    /**
799
     * update a record in the database
800
     *
801
     * @param string $gedrec
802
     * @param Tree   $tree
803
     * @param bool   $delete
804
     *
805
     * @return void
806
     * @throws GedcomErrorException
807
     */
808
    public static function updateRecord(string $gedrec, Tree $tree, bool $delete): void
809
    {
810
        if (preg_match('/^0 @(' . Gedcom::REGEX_XREF . ')@ (' . Gedcom::REGEX_TAG . ')/', $gedrec, $match)) {
811
            [, $gid, $type] = $match;
812
        } elseif (preg_match('/^0 (HEAD)(?:\n|$)/', $gedrec, $match)) {
813
            // The HEAD record has no XREF.  Any others?
814
            $gid  = $match[1];
815
            $type = $match[1];
816
        } else {
817
            throw new GedcomErrorException($gedrec);
818
        }
819
820
        // Place links
821
        DB::table('placelinks')
822
            ->where('pl_gid', '=', $gid)
823
            ->where('pl_file', '=', $tree->id())
824
            ->delete();
825
826
        // Orphaned places.  If we're deleting  "Westminster, London, England",
827
        // then we may also need to delete "London, England" and "England".
828
        do {
829
            $affected = DB::table('places')
830
                ->leftJoin('placelinks', static function (JoinClause $join): void {
831
                    $join
832
                        ->on('p_id', '=', 'pl_p_id')
833
                        ->on('p_file', '=', 'pl_file');
834
                })
835
                ->whereNull('pl_p_id')
836
                ->delete();
837
        } while ($affected > 0);
838
839
        DB::table('dates')
840
            ->where('d_gid', '=', $gid)
841
            ->where('d_file', '=', $tree->id())
842
            ->delete();
843
844
        DB::table('name')
845
            ->where('n_id', '=', $gid)
846
            ->where('n_file', '=', $tree->id())
847
            ->delete();
848
849
        DB::table('link')
850
            ->where('l_from', '=', $gid)
851
            ->where('l_file', '=', $tree->id())
852
            ->delete();
853
854
        switch ($type) {
855
            case Individual::RECORD_TYPE:
856
                DB::table('individuals')
857
                    ->where('i_id', '=', $gid)
858
                    ->where('i_file', '=', $tree->id())
859
                    ->delete();
860
                break;
861
862
            case Family::RECORD_TYPE:
863
                DB::table('families')
864
                    ->where('f_id', '=', $gid)
865
                    ->where('f_file', '=', $tree->id())
866
                    ->delete();
867
                break;
868
869
            case Source::RECORD_TYPE:
870
                DB::table('sources')
871
                    ->where('s_id', '=', $gid)
872
                    ->where('s_file', '=', $tree->id())
873
                    ->delete();
874
                break;
875
876
            case Media::RECORD_TYPE:
877
                DB::table('media_file')
878
                    ->where('m_id', '=', $gid)
879
                    ->where('m_file', '=', $tree->id())
880
                    ->delete();
881
882
                DB::table('media')
883
                    ->where('m_id', '=', $gid)
884
                    ->where('m_file', '=', $tree->id())
885
                    ->delete();
886
                break;
887
888
            default:
889
                DB::table('other')
890
                    ->where('o_id', '=', $gid)
891
                    ->where('o_file', '=', $tree->id())
892
                    ->delete();
893
                break;
894
        }
895
896
        if (!$delete) {
897
            self::importRecord($gedrec, $tree, true);
898
        }
899
    }
900
}
901