Passed
Push — master ( 4ed9f2...c8be44 )
by Greg
06:17
created

FunctionsImport::createMediaObject()   C

Complexity

Conditions 11
Paths 194

Size

Total Lines 90
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 54
c 1
b 0
f 0
nc 194
nop 2
dl 0
loc 90
rs 6.2303

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 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\Family;
25
use Fisharebest\Webtrees\Gedcom;
26
use Fisharebest\Webtrees\GedcomTag;
27
use Fisharebest\Webtrees\Header;
28
use Fisharebest\Webtrees\Individual;
29
use Fisharebest\Webtrees\Location;
30
use Fisharebest\Webtrees\Media;
31
use Fisharebest\Webtrees\Note;
32
use Fisharebest\Webtrees\Place;
33
use Fisharebest\Webtrees\PlaceLocation;
34
use Fisharebest\Webtrees\Registry;
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 app;
46
use function array_chunk;
47
use function array_intersect_key;
48
use function array_map;
49
use function array_unique;
50
use function assert;
51
use function date;
52
use function explode;
53
use function max;
54
use function preg_match;
55
use function preg_match_all;
56
use function preg_replace;
57
use function round;
58
use function str_contains;
59
use function str_replace;
60
use function str_starts_with;
61
use function strlen;
62
use function strtolower;
63
use function strtoupper;
64
use function substr;
65
use function trim;
66
67
use const PREG_SET_ORDER;
68
69
/**
70
 * Class FunctionsImport - common functions
71
 */
72
class FunctionsImport
73
{
74
    /**
75
     * Tidy up a gedcom record on import, so that we can access it consistently/efficiently.
76
     *
77
     * @param string $rec
78
     * @param Tree   $tree
79
     *
80
     * @return string
81
     */
82
    public static function reformatRecord(string $rec, Tree $tree): string
83
    {
84
        $gedcom_service = app(GedcomService::class);
85
        assert($gedcom_service instanceof GedcomService);
86
87
        // Strip out mac/msdos line endings
88
        $rec = preg_replace("/[\r\n]+/", "\n", $rec);
89
90
        // Extract lines from the record; lines consist of: level + optional xref + tag + optional data
91
        $num_matches = preg_match_all('/^[ \t]*(\d+)[ \t]*(@[^@]*@)?[ \t]*(\w+)[ \t]?(.*)$/m', $rec, $matches, PREG_SET_ORDER);
92
93
        // Process the record line-by-line
94
        $newrec = '';
95
        foreach ($matches as $n => $match) {
96
            [, $level, $xref, $tag, $data] = $match;
97
98
            $tag = $gedcom_service->canonicalTag($tag);
99
100
            switch ($tag) {
101
                case 'AFN':
102
                    // AFN values are upper case
103
                    $data = strtoupper($data);
104
                    break;
105
                case 'DATE':
106
                    // Preserve text from INT dates
107
                    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

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

228
                    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...
229
                        $data = strtr($data, ['  ' => ' ']);
230
                    }
231
                    $newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level === '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag !== 'NOTE' ? '' : ' ' . $data);
232
                    break;
233
                case 'NOTE':
234
                case 'TEXT':
235
                case 'DATA':
236
                case 'CONT':
237
                    $newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level === '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag !== 'NOTE' ? '' : ' ' . $data);
238
                    break;
239
                case 'FILE':
240
                    // Strip off the user-defined path prefix
241
                    $GEDCOM_MEDIA_PATH = $tree->getPreference('GEDCOM_MEDIA_PATH');
242
                    if ($GEDCOM_MEDIA_PATH !== '' && str_starts_with($data, $GEDCOM_MEDIA_PATH)) {
243
                        $data = substr($data, strlen($GEDCOM_MEDIA_PATH));
244
                    }
245
                    // convert backslashes in filenames to forward slashes
246
                    $data = preg_replace("/\\\\/", '/', $data);
247
248
                    $newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level === '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag !== 'NOTE' ? '' : ' ' . $data);
249
                    break;
250
                case 'CONC':
251
                    // Merge CONC lines, to simplify access later on.
252
                    $newrec .= ($tree->getPreference('WORD_WRAPPED_NOTES') ? ' ' : '') . $data;
253
                    break;
254
            }
255
        }
256
257
        return $newrec;
258
    }
259
260
    /**
261
     * import record into database
262
     * this function will parse the given gedcom record and add it to the database
263
     *
264
     * @param string $gedrec the raw gedcom record to parse
265
     * @param Tree   $tree   import the record into this tree
266
     * @param bool   $update whether or not this is an updated record that has been accepted
267
     *
268
     * @return void
269
     * @throws GedcomErrorException
270
     */
271
    public static function importRecord(string $gedrec, Tree $tree, bool $update): void
272
    {
273
        $tree_id = $tree->id();
274
275
        // Escaped @ signs (only if importing from file)
276
        if (!$update) {
277
            $gedrec = str_replace('@@', '@', $gedrec);
278
        }
279
280
        // Standardise gedcom format
281
        $gedrec = self::reformatRecord($gedrec, $tree);
282
283
        // import different types of records
284
        if (preg_match('/^0 @(' . Gedcom::REGEX_XREF . ')@ (' . Gedcom::REGEX_TAG . ')/', $gedrec, $match)) {
285
            [, $xref, $type] = $match;
286
            // check for a _UID, if the record doesn't have one, add one
287
            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

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

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