1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* webtrees: online genealogy |
4
|
|
|
* Copyright (C) 2019 webtrees development team |
5
|
|
|
* This program is free software: you can redistribute it and/or modify |
6
|
|
|
* it under the terms of the GNU General Public License as published by |
7
|
|
|
* the Free Software Foundation, either version 3 of the License, or |
8
|
|
|
* (at your option) any later version. |
9
|
|
|
* This program is distributed in the hope that it will be useful, |
10
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
11
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12
|
|
|
* GNU General Public License for more details. |
13
|
|
|
* You should have received a copy of the GNU General Public License |
14
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
15
|
|
|
*/ |
16
|
|
|
namespace Fisharebest\Webtrees\Functions; |
17
|
|
|
|
18
|
|
|
use Fisharebest\Webtrees\Database; |
19
|
|
|
use Fisharebest\Webtrees\Date; |
20
|
|
|
use Fisharebest\Webtrees\GedcomRecord; |
21
|
|
|
use Fisharebest\Webtrees\GedcomTag; |
22
|
|
|
use Fisharebest\Webtrees\I18N; |
23
|
|
|
use Fisharebest\Webtrees\Individual; |
24
|
|
|
use Fisharebest\Webtrees\Log; |
25
|
|
|
use Fisharebest\Webtrees\Media; |
26
|
|
|
use Fisharebest\Webtrees\Note; |
27
|
|
|
use Fisharebest\Webtrees\Repository; |
28
|
|
|
use Fisharebest\Webtrees\Soundex; |
29
|
|
|
use Fisharebest\Webtrees\Source; |
30
|
|
|
use Fisharebest\Webtrees\Tree; |
31
|
|
|
use PDOException; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Class FunctionsImport - common functions |
35
|
|
|
*/ |
36
|
|
|
class FunctionsImport |
37
|
|
|
{ |
38
|
|
|
/** |
39
|
|
|
* Tidy up a gedcom record on import, so that we can access it consistently/efficiently. |
40
|
|
|
* |
41
|
|
|
* @param string $rec |
42
|
|
|
* @param Tree $tree |
43
|
|
|
* |
44
|
|
|
* @return string |
45
|
|
|
*/ |
46
|
|
|
public static function reformatRecord($rec, Tree $tree) |
47
|
|
|
{ |
48
|
|
|
// Strip out UTF8 formatting characters |
49
|
|
|
$rec = str_replace(array(WT_UTF8_BOM, WT_UTF8_LRM, WT_UTF8_RLM), '', $rec); |
50
|
|
|
|
51
|
|
|
// Strip out mac/msdos line endings |
52
|
|
|
$rec = preg_replace("/[\r\n]+/", "\n", $rec); |
53
|
|
|
|
54
|
|
|
// Extract lines from the record; lines consist of: level + optional xref + tag + optional data |
55
|
|
|
$num_matches = preg_match_all('/^[ \t]*(\d+)[ \t]*(@[^@]*@)?[ \t]*(\w+)[ \t]?(.*)$/m', $rec, $matches, PREG_SET_ORDER); |
56
|
|
|
|
57
|
|
|
// Process the record line-by-line |
58
|
|
|
$newrec = ''; |
59
|
|
|
foreach ($matches as $n => $match) { |
60
|
|
|
list(, $level, $xref, $tag, $data) = $match; |
61
|
|
|
$tag = strtoupper($tag); // Tags should always be upper case |
62
|
|
|
switch ($tag) { |
63
|
|
|
// Convert PhpGedView tags to WT |
64
|
|
|
case '_PGVU': |
65
|
|
|
$tag = '_WT_USER'; |
66
|
|
|
break; |
67
|
|
|
case '_PGV_OBJS': |
68
|
|
|
$tag = '_WT_OBJE_SORT'; |
69
|
|
|
break; |
70
|
|
|
// Convert FTM-style "TAG_FORMAL_NAME" into "TAG". |
71
|
|
|
case 'ABBREVIATION': |
72
|
|
|
$tag = 'ABBR'; |
73
|
|
|
break; |
74
|
|
|
case 'ADDRESS': |
75
|
|
|
$tag = 'ADDR'; |
76
|
|
|
break; |
77
|
|
|
case 'ADDRESS1': |
78
|
|
|
$tag = 'ADR1'; |
79
|
|
|
break; |
80
|
|
|
case 'ADDRESS2': |
81
|
|
|
$tag = 'ADR2'; |
82
|
|
|
break; |
83
|
|
|
case 'ADDRESS3': |
84
|
|
|
$tag = 'ADR3'; |
85
|
|
|
break; |
86
|
|
|
case 'ADOPTION': |
87
|
|
|
$tag = 'ADOP'; |
88
|
|
|
break; |
89
|
|
|
case 'ADULT_CHRISTENING': |
90
|
|
|
$tag = 'CHRA'; |
91
|
|
|
break; |
92
|
|
|
case 'AFN': |
93
|
|
|
// AFN values are upper case |
94
|
|
|
$data = strtoupper($data); |
95
|
|
|
break; |
96
|
|
|
case 'AGENCY': |
97
|
|
|
$tag = 'AGNC'; |
98
|
|
|
break; |
99
|
|
|
case 'ALIAS': |
100
|
|
|
$tag = 'ALIA'; |
101
|
|
|
break; |
102
|
|
|
case 'ANCESTORS': |
103
|
|
|
$tag = 'ANCE'; |
104
|
|
|
break; |
105
|
|
|
case 'ANCES_INTEREST': |
106
|
|
|
$tag = 'ANCI'; |
107
|
|
|
break; |
108
|
|
|
case 'ANNULMENT': |
109
|
|
|
$tag = 'ANUL'; |
110
|
|
|
break; |
111
|
|
|
case 'ASSOCIATES': |
112
|
|
|
$tag = 'ASSO'; |
113
|
|
|
break; |
114
|
|
|
case 'AUTHOR': |
115
|
|
|
$tag = 'AUTH'; |
116
|
|
|
break; |
117
|
|
|
case 'BAPTISM': |
118
|
|
|
$tag = 'BAPM'; |
119
|
|
|
break; |
120
|
|
|
case 'BAPTISM_LDS': |
121
|
|
|
$tag = 'BAPL'; |
122
|
|
|
break; |
123
|
|
|
case 'BAR_MITZVAH': |
124
|
|
|
$tag = 'BARM'; |
125
|
|
|
break; |
126
|
|
|
case 'BAS_MITZVAH': |
127
|
|
|
$tag = 'BASM'; |
128
|
|
|
break; |
129
|
|
|
case 'BIRTH': |
130
|
|
|
$tag = 'BIRT'; |
131
|
|
|
break; |
132
|
|
|
case 'BLESSING': |
133
|
|
|
$tag = 'BLES'; |
134
|
|
|
break; |
135
|
|
|
case 'BURIAL': |
136
|
|
|
$tag = 'BURI'; |
137
|
|
|
break; |
138
|
|
|
case 'CALL_NUMBER': |
139
|
|
|
$tag = 'CALN'; |
140
|
|
|
break; |
141
|
|
|
case 'CASTE': |
142
|
|
|
$tag = 'CAST'; |
143
|
|
|
break; |
144
|
|
|
case 'CAUSE': |
145
|
|
|
$tag = 'CAUS'; |
146
|
|
|
break; |
147
|
|
|
case 'CENSUS': |
148
|
|
|
$tag = 'CENS'; |
149
|
|
|
break; |
150
|
|
|
case 'CHANGE': |
151
|
|
|
$tag = 'CHAN'; |
152
|
|
|
break; |
153
|
|
|
case 'CHARACTER': |
154
|
|
|
$tag = 'CHAR'; |
155
|
|
|
break; |
156
|
|
|
case 'CHILD': |
157
|
|
|
$tag = 'CHIL'; |
158
|
|
|
break; |
159
|
|
|
case 'CHILDREN_COUNT': |
160
|
|
|
$tag = 'NCHI'; |
161
|
|
|
break; |
162
|
|
|
case 'CHRISTENING': |
163
|
|
|
$tag = 'CHR'; |
164
|
|
|
break; |
165
|
|
|
case 'CONCATENATION': |
166
|
|
|
$tag = 'CONC'; |
167
|
|
|
break; |
168
|
|
|
case 'CONFIRMATION': |
169
|
|
|
$tag = 'CONF'; |
170
|
|
|
break; |
171
|
|
|
case 'CONFIRMATION_LDS': |
172
|
|
|
$tag = 'CONL'; |
173
|
|
|
break; |
174
|
|
|
case 'CONTINUED': |
175
|
|
|
$tag = 'CONT'; |
176
|
|
|
break; |
177
|
|
|
case 'COPYRIGHT': |
178
|
|
|
$tag = 'COPR'; |
179
|
|
|
break; |
180
|
|
|
case 'CORPORATE': |
181
|
|
|
$tag = 'CORP'; |
182
|
|
|
break; |
183
|
|
|
case 'COUNTRY': |
184
|
|
|
$tag = 'CTRY'; |
185
|
|
|
break; |
186
|
|
|
case 'CREMATION': |
187
|
|
|
$tag = 'CREM'; |
188
|
|
|
break; |
189
|
|
|
case 'DATE': |
190
|
|
|
// Preserve text from INT dates |
191
|
|
|
if (strpos($data, '(') !== false) { |
192
|
|
|
list($date, $text) = explode('(', $data, 2); |
193
|
|
|
$text = ' (' . $text; |
194
|
|
|
} else { |
195
|
|
|
$date = $data; |
196
|
|
|
$text = ''; |
197
|
|
|
} |
198
|
|
|
// Capitals |
199
|
|
|
$date = strtoupper($date); |
200
|
|
|
// Temporarily add leading/trailing spaces, to allow efficient matching below |
201
|
|
|
$date = " {$date} "; |
202
|
|
|
// Ensure space digits and letters |
203
|
|
|
$date = preg_replace('/([A-Z])(\d)/', '$1 $2', $date); |
204
|
|
|
$date = preg_replace('/(\d)([A-Z])/', '$1 $2', $date); |
205
|
|
|
// Ensure space before/after calendar escapes |
206
|
|
|
$date = preg_replace('/@#[^@]+@/', ' $0 ', $date); |
207
|
|
|
// "BET." => "BET" |
208
|
|
|
$date = preg_replace('/(\w\w)\./', '$1', $date); |
209
|
|
|
// "CIR" => "ABT" |
210
|
|
|
$date = str_replace(' CIR ', ' ABT ', $date); |
211
|
|
|
$date = str_replace(' APX ', ' ABT ', $date); |
212
|
|
|
// B.C. => BC (temporarily, to allow easier handling of ".") |
213
|
|
|
$date = str_replace(' B.C. ', ' BC ', $date); |
214
|
|
|
// "BET X - Y " => "BET X AND Y" |
215
|
|
|
$date = preg_replace('/^(.* BET .+) - (.+)/', '$1 AND $2', $date); |
216
|
|
|
$date = preg_replace('/^(.* FROM .+) - (.+)/', '$1 TO $2', $date); |
217
|
|
|
// "@#ESC@ FROM X TO Y" => "FROM @#ESC@ X TO @#ESC@ Y" |
218
|
|
|
$date = preg_replace('/^ +(@#[^@]+@) +FROM +(.+) +TO +(.+)/', ' FROM $1 $2 TO $1 $3', $date); |
219
|
|
|
$date = preg_replace('/^ +(@#[^@]+@) +BET +(.+) +AND +(.+)/', ' BET $1 $2 AND $1 $3', $date); |
220
|
|
|
// "@#ESC@ AFT X" => "AFT @#ESC@ X" |
221
|
|
|
$date = preg_replace('/^ +(@#[^@]+@) +(FROM|BET|TO|AND|BEF|AFT|CAL|EST|INT|ABT) +(.+)/', ' $2 $1 $3', $date); |
222
|
|
|
// Ignore any remaining punctuation, e.g. "14-MAY, 1900" => "14 MAY 1900" |
223
|
|
|
// (don't change "/" - it is used in NS/OS dates) |
224
|
|
|
$date = preg_replace('/[.,:;-]/', ' ', $date); |
225
|
|
|
// BC => B.C. |
226
|
|
|
$date = str_replace(' BC ', ' B.C. ', $date); |
227
|
|
|
// Append the "INT" text |
228
|
|
|
$data = $date . $text; |
229
|
|
|
break; |
230
|
|
|
case 'DEATH': |
231
|
|
|
$tag = 'DEAT'; |
232
|
|
|
break; |
233
|
|
|
case '_DEATH_OF_SPOUSE': |
234
|
|
|
$tag = '_DETS'; |
235
|
|
|
break; |
236
|
|
|
case '_DEGREE': |
237
|
|
|
$tag = '_DEG'; |
238
|
|
|
break; |
239
|
|
|
case 'DESCENDANTS': |
240
|
|
|
$tag = 'DESC'; |
241
|
|
|
break; |
242
|
|
|
case 'DESCENDANT_INT': |
243
|
|
|
$tag = 'DESI'; |
244
|
|
|
break; |
245
|
|
|
case 'DESTINATION': |
246
|
|
|
$tag = 'DEST'; |
247
|
|
|
break; |
248
|
|
|
case 'DIVORCE': |
249
|
|
|
$tag = 'DIV'; |
250
|
|
|
break; |
251
|
|
|
case 'DIVORCE_FILED': |
252
|
|
|
$tag = 'DIVF'; |
253
|
|
|
break; |
254
|
|
|
case 'EDUCATION': |
255
|
|
|
$tag = 'EDUC'; |
256
|
|
|
break; |
257
|
|
|
case 'EMIGRATION': |
258
|
|
|
$tag = 'EMIG'; |
259
|
|
|
break; |
260
|
|
|
case 'ENDOWMENT': |
261
|
|
|
$tag = 'ENDL'; |
262
|
|
|
break; |
263
|
|
|
case 'ENGAGEMENT': |
264
|
|
|
$tag = 'ENGA'; |
265
|
|
|
break; |
266
|
|
|
case 'EVENT': |
267
|
|
|
$tag = 'EVEN'; |
268
|
|
|
break; |
269
|
|
|
case 'FACSIMILE': |
270
|
|
|
$tag = 'FAX'; |
271
|
|
|
break; |
272
|
|
|
case 'FAMILY': |
273
|
|
|
$tag = 'FAM'; |
274
|
|
|
break; |
275
|
|
|
case 'FAMILY_CHILD': |
276
|
|
|
$tag = 'FAMC'; |
277
|
|
|
break; |
278
|
|
|
case 'FAMILY_FILE': |
279
|
|
|
$tag = 'FAMF'; |
280
|
|
|
break; |
281
|
|
|
case 'FAMILY_SPOUSE': |
282
|
|
|
$tag = 'FAMS'; |
283
|
|
|
break; |
284
|
|
|
case 'FIRST_COMMUNION': |
285
|
|
|
$tag = 'FCOM'; |
286
|
|
|
break; |
287
|
|
|
case '_FILE': |
288
|
|
|
$tag = 'FILE'; |
289
|
|
|
break; |
290
|
|
|
case 'FORMAT': |
291
|
|
|
$tag = 'FORM'; |
|
|
|
|
292
|
|
|
case 'FORM': |
293
|
|
|
// Consistent commas |
294
|
|
|
$data = preg_replace('/ *, */', ', ', $data); |
295
|
|
|
break; |
296
|
|
|
case 'GEDCOM': |
297
|
|
|
$tag = 'GEDC'; |
298
|
|
|
break; |
299
|
|
|
case 'GIVEN_NAME': |
300
|
|
|
$tag = 'GIVN'; |
301
|
|
|
break; |
302
|
|
|
case 'GRADUATION': |
303
|
|
|
$tag = 'GRAD'; |
304
|
|
|
break; |
305
|
|
|
case 'HEADER': |
306
|
|
|
$tag = 'HEAD'; |
|
|
|
|
307
|
|
|
case 'HEAD': |
308
|
|
|
// HEAD records don't have an XREF or DATA |
309
|
|
|
if ($level == '0') { |
310
|
|
|
$xref = ''; |
311
|
|
|
$data = ''; |
312
|
|
|
} |
313
|
|
|
break; |
314
|
|
|
case 'HUSBAND': |
315
|
|
|
$tag = 'HUSB'; |
316
|
|
|
break; |
317
|
|
|
case 'IDENT_NUMBER': |
318
|
|
|
$tag = 'IDNO'; |
319
|
|
|
break; |
320
|
|
|
case 'IMMIGRATION': |
321
|
|
|
$tag = 'IMMI'; |
322
|
|
|
break; |
323
|
|
|
case 'INDIVIDUAL': |
324
|
|
|
$tag = 'INDI'; |
325
|
|
|
break; |
326
|
|
|
case 'LANGUAGE': |
327
|
|
|
$tag = 'LANG'; |
328
|
|
|
break; |
329
|
|
|
case 'LATITUDE': |
330
|
|
|
$tag = 'LATI'; |
331
|
|
|
break; |
332
|
|
|
case 'LONGITUDE': |
333
|
|
|
$tag = 'LONG'; |
334
|
|
|
break; |
335
|
|
|
case 'MARRIAGE': |
336
|
|
|
$tag = 'MARR'; |
337
|
|
|
break; |
338
|
|
|
case 'MARRIAGE_BANN': |
339
|
|
|
$tag = 'MARB'; |
340
|
|
|
break; |
341
|
|
|
case 'MARRIAGE_COUNT': |
342
|
|
|
$tag = 'NMR'; |
343
|
|
|
break; |
344
|
|
|
case 'MARRIAGE_CONTRACT': |
345
|
|
|
$tag = 'MARC'; |
346
|
|
|
break; |
347
|
|
|
case 'MARRIAGE_LICENSE': |
348
|
|
|
$tag = 'MARL'; |
349
|
|
|
break; |
350
|
|
|
case 'MARRIAGE_SETTLEMENT': |
351
|
|
|
$tag = 'MARS'; |
352
|
|
|
break; |
353
|
|
|
case 'MEDIA': |
354
|
|
|
$tag = 'MEDI'; |
355
|
|
|
break; |
356
|
|
|
case '_MEDICAL': |
357
|
|
|
$tag = '_MDCL'; |
358
|
|
|
break; |
359
|
|
|
case '_MILITARY_SERVICE': |
360
|
|
|
$tag = '_MILT'; |
361
|
|
|
break; |
362
|
|
|
case 'NAME': |
363
|
|
|
// Tidy up whitespace |
364
|
|
|
$data = preg_replace('/ +/', ' ', trim($data)); |
365
|
|
|
break; |
366
|
|
|
case 'NAME_PREFIX': |
367
|
|
|
$tag = 'NPFX'; |
368
|
|
|
break; |
369
|
|
|
case 'NAME_SUFFIX': |
370
|
|
|
$tag = 'NSFX'; |
371
|
|
|
break; |
372
|
|
|
case 'NATIONALITY': |
373
|
|
|
$tag = 'NATI'; |
374
|
|
|
break; |
375
|
|
|
case 'NATURALIZATION': |
376
|
|
|
$tag = 'NATU'; |
377
|
|
|
break; |
378
|
|
|
case 'NICKNAME': |
379
|
|
|
$tag = 'NICK'; |
380
|
|
|
break; |
381
|
|
|
case 'OBJECT': |
382
|
|
|
$tag = 'OBJE'; |
383
|
|
|
break; |
384
|
|
|
case 'OCCUPATION': |
385
|
|
|
$tag = 'OCCU'; |
386
|
|
|
break; |
387
|
|
|
case 'ORDINANCE': |
388
|
|
|
$tag = 'ORDI'; |
389
|
|
|
break; |
390
|
|
|
case 'ORDINATION': |
391
|
|
|
$tag = 'ORDN'; |
392
|
|
|
break; |
393
|
|
|
case 'PEDIGREE': |
394
|
|
|
$tag = 'PEDI'; |
|
|
|
|
395
|
|
|
case 'PEDI': |
396
|
|
|
// PEDI values are lower case |
397
|
|
|
$data = strtolower($data); |
398
|
|
|
break; |
399
|
|
|
case 'PHONE': |
400
|
|
|
$tag = 'PHON'; |
401
|
|
|
break; |
402
|
|
|
case 'PHONETIC': |
403
|
|
|
$tag = 'FONE'; |
404
|
|
|
break; |
405
|
|
|
case 'PHY_DESCRIPTION': |
406
|
|
|
$tag = 'DSCR'; |
407
|
|
|
break; |
408
|
|
|
case 'PLACE': |
409
|
|
|
$tag = 'PLAC'; |
|
|
|
|
410
|
|
|
case 'PLAC': |
411
|
|
|
// Consistent commas |
412
|
|
|
$data = preg_replace('/ *(،|,) */', ', ', $data); |
413
|
|
|
// The Master Genealogist stores LAT/LONG data in the PLAC field, e.g. Pennsylvania, USA, 395945N0751013W |
414
|
|
|
if (preg_match('/(.*), (\d\d)(\d\d)(\d\d)([NS])(\d\d\d)(\d\d)(\d\d)([EW])$/', $data, $match)) { |
415
|
|
|
$data = |
416
|
|
|
$match[1] . "\n" . |
417
|
|
|
($level + 1) . " MAP\n" . |
418
|
|
|
($level + 2) . " LATI " . ($match[5] . (round($match[2] + ($match[3] / 60) + ($match[4] / 3600), 4))) . "\n" . |
419
|
|
|
($level + 2) . " LONG " . ($match[9] . (round($match[6] + ($match[7] / 60) + ($match[8] / 3600), 4))); |
420
|
|
|
} |
421
|
|
|
break; |
422
|
|
|
case 'POSTAL_CODE': |
423
|
|
|
$tag = 'POST'; |
424
|
|
|
break; |
425
|
|
|
case 'PROBATE': |
426
|
|
|
$tag = 'PROB'; |
427
|
|
|
break; |
428
|
|
|
case 'PROPERTY': |
429
|
|
|
$tag = 'PROP'; |
430
|
|
|
break; |
431
|
|
|
case 'PUBLICATION': |
432
|
|
|
$tag = 'PUBL'; |
433
|
|
|
break; |
434
|
|
|
case 'QUALITY_OF_DATA': |
435
|
|
|
$tag = 'QUAL'; |
436
|
|
|
break; |
437
|
|
|
case 'REC_FILE_NUMBER': |
438
|
|
|
$tag = 'RFN'; |
439
|
|
|
break; |
440
|
|
|
case 'REC_ID_NUMBER': |
441
|
|
|
$tag = 'RIN'; |
442
|
|
|
break; |
443
|
|
|
case 'REFERENCE': |
444
|
|
|
$tag = 'REFN'; |
445
|
|
|
break; |
446
|
|
|
case 'RELATIONSHIP': |
447
|
|
|
$tag = 'RELA'; |
448
|
|
|
break; |
449
|
|
|
case 'RELIGION': |
450
|
|
|
$tag = 'RELI'; |
451
|
|
|
break; |
452
|
|
|
case 'REPOSITORY': |
453
|
|
|
$tag = 'REPO'; |
454
|
|
|
break; |
455
|
|
|
case 'RESIDENCE': |
456
|
|
|
$tag = 'RESI'; |
457
|
|
|
break; |
458
|
|
|
case 'RESTRICTION': |
459
|
|
|
$tag = 'RESN'; |
|
|
|
|
460
|
|
|
case 'RESN': |
461
|
|
|
// RESN values are lower case (confidential, privacy, locked, none) |
462
|
|
|
$data = strtolower($data); |
463
|
|
|
if ($data == 'invisible') { |
464
|
|
|
$data = 'confidential'; // From old versions of Legacy. |
465
|
|
|
} |
466
|
|
|
break; |
467
|
|
|
case 'RETIREMENT': |
468
|
|
|
$tag = 'RETI'; |
469
|
|
|
break; |
470
|
|
|
case 'ROMANIZED': |
471
|
|
|
$tag = 'ROMN'; |
472
|
|
|
break; |
473
|
|
|
case 'SEALING_CHILD': |
474
|
|
|
$tag = 'SLGC'; |
475
|
|
|
break; |
476
|
|
|
case 'SEALING_SPOUSE': |
477
|
|
|
$tag = 'SLGS'; |
478
|
|
|
break; |
479
|
|
|
case 'SOC_SEC_NUMBER': |
480
|
|
|
$tag = 'SSN'; |
481
|
|
|
break; |
482
|
|
|
case 'SEX': |
483
|
|
|
$data = strtoupper($data); |
484
|
|
|
break; |
485
|
|
|
case 'SOURCE': |
486
|
|
|
$tag = 'SOUR'; |
487
|
|
|
break; |
488
|
|
|
case 'STATE': |
489
|
|
|
$tag = 'STAE'; |
490
|
|
|
break; |
491
|
|
|
case 'STATUS': |
492
|
|
|
$tag = 'STAT'; |
|
|
|
|
493
|
|
|
case 'STAT': |
494
|
|
|
if ($data == 'CANCELLED') { |
495
|
|
|
// PhpGedView mis-spells this tag - correct it. |
496
|
|
|
$data = 'CANCELED'; |
497
|
|
|
} |
498
|
|
|
break; |
499
|
|
|
case 'SUBMISSION': |
500
|
|
|
$tag = 'SUBN'; |
501
|
|
|
break; |
502
|
|
|
case 'SUBMITTER': |
503
|
|
|
$tag = 'SUBM'; |
504
|
|
|
break; |
505
|
|
|
case 'SURNAME': |
506
|
|
|
$tag = 'SURN'; |
507
|
|
|
break; |
508
|
|
|
case 'SURN_PREFIX': |
509
|
|
|
$tag = 'SPFX'; |
510
|
|
|
break; |
511
|
|
|
case 'TEMPLE': |
512
|
|
|
$tag = 'TEMP'; |
|
|
|
|
513
|
|
|
case 'TEMP': |
514
|
|
|
// Temple codes are upper case |
515
|
|
|
$data = strtoupper($data); |
516
|
|
|
break; |
517
|
|
|
case 'TITLE': |
518
|
|
|
$tag = 'TITL'; |
519
|
|
|
break; |
520
|
|
|
case 'TRAILER': |
521
|
|
|
$tag = 'TRLR'; |
|
|
|
|
522
|
|
|
case 'TRLR': |
523
|
|
|
// TRLR records don't have an XREF or DATA |
524
|
|
|
if ($level == '0') { |
525
|
|
|
$xref = ''; |
526
|
|
|
$data = ''; |
527
|
|
|
} |
528
|
|
|
break; |
529
|
|
|
case 'VERSION': |
530
|
|
|
$tag = 'VERS'; |
531
|
|
|
break; |
532
|
|
|
case 'WEB': |
533
|
|
|
$tag = 'WWW'; |
534
|
|
|
break; |
535
|
|
|
} |
536
|
|
|
// Suppress "Y", for facts/events with a DATE or PLAC |
537
|
|
|
if ($data == 'y') { |
538
|
|
|
$data = 'Y'; |
539
|
|
|
} |
540
|
|
|
if ($level == '1' && $data == 'Y') { |
541
|
|
|
for ($i = $n + 1; $i < $num_matches - 1 && $matches[$i][1] != '1'; ++$i) { |
542
|
|
|
if ($matches[$i][3] == 'DATE' || $matches[$i][3] == 'PLAC') { |
543
|
|
|
$data = ''; |
544
|
|
|
break; |
545
|
|
|
} |
546
|
|
|
} |
547
|
|
|
} |
548
|
|
|
// Reassemble components back into a single line |
549
|
|
|
switch ($tag) { |
550
|
|
|
default: |
551
|
|
|
// Remove tabs and multiple/leading/trailing spaces |
552
|
|
|
if (strpos($data, "\t") !== false) { |
553
|
|
|
$data = str_replace("\t", ' ', $data); |
554
|
|
|
} |
555
|
|
|
if (substr($data, 0, 1) == ' ' || substr($data, -1, 1) == ' ') { |
556
|
|
|
$data = trim($data); |
557
|
|
|
} |
558
|
|
|
while (strpos($data, ' ')) { |
559
|
|
|
$data = str_replace(' ', ' ', $data); |
560
|
|
|
} |
561
|
|
|
$newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level == '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag != "NOTE" ? '' : ' ' . $data); |
562
|
|
|
break; |
563
|
|
|
case 'NOTE': |
564
|
|
|
case 'TEXT': |
565
|
|
|
case 'DATA': |
566
|
|
|
case 'CONT': |
567
|
|
|
$newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level == '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag != "NOTE" ? '' : ' ' . $data); |
568
|
|
|
break; |
569
|
|
|
case 'FILE': |
570
|
|
|
// Strip off the user-defined path prefix |
571
|
|
|
$GEDCOM_MEDIA_PATH = $tree->getPreference('GEDCOM_MEDIA_PATH'); |
572
|
|
|
if ($GEDCOM_MEDIA_PATH && strpos($data, $GEDCOM_MEDIA_PATH) === 0) { |
573
|
|
|
$data = substr($data, strlen($GEDCOM_MEDIA_PATH)); |
574
|
|
|
} |
575
|
|
|
// convert backslashes in filenames to forward slashes |
576
|
|
|
$data = preg_replace("/\\\/", "/", $data); |
577
|
|
|
|
578
|
|
|
$newrec .= ($newrec ? "\n" : '') . $level . ' ' . ($level == '0' && $xref ? $xref . ' ' : '') . $tag . ($data === '' && $tag != "NOTE" ? '' : ' ' . $data); |
579
|
|
|
break; |
580
|
|
|
case 'CONC': |
581
|
|
|
// Merge CONC lines, to simplify access later on. |
582
|
|
|
$newrec .= ($tree->getPreference('WORD_WRAPPED_NOTES') ? ' ' : '') . $data; |
583
|
|
|
break; |
584
|
|
|
} |
585
|
|
|
} |
586
|
|
|
|
587
|
|
|
return $newrec; |
588
|
|
|
} |
589
|
|
|
|
590
|
|
|
/** |
591
|
|
|
* import record into database |
592
|
|
|
* |
593
|
|
|
* this function will parse the given gedcom record and add it to the database |
594
|
|
|
* |
595
|
|
|
* @param string $gedrec the raw gedcom record to parse |
596
|
|
|
* @param Tree $tree import the record into this tree |
597
|
|
|
* @param bool $update whether or not this is an updated record that has been accepted |
598
|
|
|
*/ |
599
|
|
|
public static function importRecord($gedrec, Tree $tree, $update) |
600
|
|
|
{ |
601
|
|
|
$tree_id = $tree->getTreeId(); |
602
|
|
|
|
603
|
|
|
// Escaped @ signs (only if importing from file) |
604
|
|
|
if (!$update) { |
605
|
|
|
$gedrec = str_replace('@@', '@', $gedrec); |
606
|
|
|
} |
607
|
|
|
|
608
|
|
|
// Standardise gedcom format |
609
|
|
|
$gedrec = self::reformatRecord($gedrec, $tree); |
610
|
|
|
|
611
|
|
|
// import different types of records |
612
|
|
|
if (preg_match('/^0 @(' . WT_REGEX_XREF . ')@ (' . WT_REGEX_TAG . ')/', $gedrec, $match)) { |
613
|
|
|
list(, $xref, $type) = $match; |
614
|
|
|
// check for a _UID, if the record doesn't have one, add one |
615
|
|
|
if ($tree->getPreference('GENERATE_UIDS') && !strpos($gedrec, "\n1 _UID ")) { |
616
|
|
|
$gedrec .= "\n1 _UID " . GedcomTag::createUid(); |
617
|
|
|
} |
618
|
|
|
} elseif (preg_match('/0 (HEAD|TRLR)/', $gedrec, $match)) { |
619
|
|
|
$type = $match[1]; |
620
|
|
|
$xref = $type; // For HEAD/TRLR, use type as pseudo XREF. |
621
|
|
|
} else { |
622
|
|
|
echo I18N::translate('Invalid GEDCOM format'), '<br><pre>', $gedrec, '</pre>'; |
623
|
|
|
|
624
|
|
|
return; |
625
|
|
|
} |
626
|
|
|
|
627
|
|
|
// If the user has downloaded their GEDCOM data (containing media objects) and edited it |
628
|
|
|
// using an application which does not support (and deletes) media objects, then add them |
629
|
|
|
// back in. |
630
|
|
|
if ($tree->getPreference('keep_media') && $xref) { |
631
|
|
|
$old_linked_media = |
632
|
|
|
Database::prepare("SELECT l_to FROM `##link` WHERE l_from=? AND l_file=? AND l_type='OBJE'") |
633
|
|
|
->execute(array($xref, $tree_id)) |
634
|
|
|
->fetchOneColumn(); |
635
|
|
|
foreach ($old_linked_media as $media_id) { |
636
|
|
|
$gedrec .= "\n1 OBJE @" . $media_id . "@"; |
637
|
|
|
} |
638
|
|
|
} |
639
|
|
|
|
640
|
|
|
switch ($type) { |
641
|
|
|
case 'INDI': |
642
|
|
|
// Convert inline media into media objects |
643
|
|
|
$gedrec = self::convertInlineMedia($tree, $gedrec); |
644
|
|
|
|
645
|
|
|
$record = new Individual($xref, $gedrec, null, $tree); |
646
|
|
|
if (preg_match('/\n1 RIN (.+)/', $gedrec, $match)) { |
647
|
|
|
$rin = $match[1]; |
648
|
|
|
} else { |
649
|
|
|
$rin = $xref; |
650
|
|
|
} |
651
|
|
|
Database::prepare( |
652
|
|
|
"INSERT INTO `##individuals` (i_id, i_file, i_rin, i_sex, i_gedcom) VALUES (?, ?, ?, ?, ?)" |
653
|
|
|
)->execute(array( |
654
|
|
|
$xref, $tree_id, $rin, $record->getSex(), $gedrec, |
655
|
|
|
)); |
656
|
|
|
// Update the cross-reference/index tables. |
657
|
|
|
self::updatePlaces($xref, $tree_id, $gedrec); |
658
|
|
|
self::updateDates($xref, $tree_id, $gedrec); |
659
|
|
|
self::updateLinks($xref, $tree_id, $gedrec); |
660
|
|
|
self::updateNames($xref, $tree_id, $record); |
661
|
|
|
break; |
662
|
|
|
case 'FAM': |
663
|
|
|
// Convert inline media into media objects |
664
|
|
|
$gedrec = self::convertInlineMedia($tree, $gedrec); |
665
|
|
|
|
666
|
|
|
if (preg_match('/\n1 HUSB @(' . WT_REGEX_XREF . ')@/', $gedrec, $match)) { |
667
|
|
|
$husb = $match[1]; |
668
|
|
|
} else { |
669
|
|
|
$husb = ''; |
670
|
|
|
} |
671
|
|
|
if (preg_match('/\n1 WIFE @(' . WT_REGEX_XREF . ')@/', $gedrec, $match)) { |
672
|
|
|
$wife = $match[1]; |
673
|
|
|
} else { |
674
|
|
|
$wife = ''; |
675
|
|
|
} |
676
|
|
|
$nchi = preg_match_all('/\n1 CHIL @(' . WT_REGEX_XREF . ')@/', $gedrec, $match); |
677
|
|
|
if (preg_match('/\n1 NCHI (\d+)/', $gedrec, $match)) { |
678
|
|
|
$nchi = max($nchi, $match[1]); |
679
|
|
|
} |
680
|
|
|
Database::prepare( |
681
|
|
|
"INSERT INTO `##families` (f_id, f_file, f_husb, f_wife, f_gedcom, f_numchil) VALUES (?, ?, ?, ?, ?, ?)" |
682
|
|
|
)->execute(array( |
683
|
|
|
$xref, $tree_id, $husb, $wife, $gedrec, $nchi, |
684
|
|
|
)); |
685
|
|
|
// Update the cross-reference/index tables. |
686
|
|
|
self::updatePlaces($xref, $tree_id, $gedrec); |
687
|
|
|
self::updateDates($xref, $tree_id, $gedrec); |
688
|
|
|
self::updateLinks($xref, $tree_id, $gedrec); |
689
|
|
|
break; |
690
|
|
|
case 'SOUR': |
691
|
|
|
// Convert inline media into media objects |
692
|
|
|
$gedrec = self::convertInlineMedia($tree, $gedrec); |
693
|
|
|
|
694
|
|
|
$record = new Source($xref, $gedrec, null, $tree); |
695
|
|
|
if (preg_match('/\n1 TITL (.+)/', $gedrec, $match)) { |
696
|
|
|
$name = $match[1]; |
697
|
|
|
} elseif (preg_match('/\n1 ABBR (.+)/', $gedrec, $match)) { |
698
|
|
|
$name = $match[1]; |
699
|
|
|
} else { |
700
|
|
|
$name = $xref; |
701
|
|
|
} |
702
|
|
|
Database::prepare( |
703
|
|
|
"INSERT INTO `##sources` (s_id, s_file, s_name, s_gedcom) VALUES (?, ?, LEFT(?, 255), ?)" |
704
|
|
|
)->execute(array( |
705
|
|
|
$xref, $tree_id, $name, $gedrec, |
706
|
|
|
)); |
707
|
|
|
// Update the cross-reference/index tables. |
708
|
|
|
self::updateLinks($xref, $tree_id, $gedrec); |
709
|
|
|
self::updateNames($xref, $tree_id, $record); |
710
|
|
|
break; |
711
|
|
|
case 'REPO': |
712
|
|
|
// Convert inline media into media objects |
713
|
|
|
$gedrec = self::convertInlineMedia($tree, $gedrec); |
714
|
|
|
|
715
|
|
|
$record = new Repository($xref, $gedrec, null, $tree); |
716
|
|
|
Database::prepare( |
717
|
|
|
"INSERT INTO `##other` (o_id, o_file, o_type, o_gedcom) VALUES (?, ?, 'REPO', ?)" |
718
|
|
|
)->execute(array( |
719
|
|
|
$xref, $tree_id, $gedrec, |
720
|
|
|
)); |
721
|
|
|
// Update the cross-reference/index tables. |
722
|
|
|
self::updateLinks($xref, $tree_id, $gedrec); |
723
|
|
|
self::updateNames($xref, $tree_id, $record); |
724
|
|
|
break; |
725
|
|
|
case 'NOTE': |
726
|
|
|
$record = new Note($xref, $gedrec, null, $tree); |
727
|
|
|
Database::prepare( |
728
|
|
|
"INSERT INTO `##other` (o_id, o_file, o_type, o_gedcom) VALUES (?, ?, 'NOTE', ?)" |
729
|
|
|
)->execute(array( |
730
|
|
|
$xref, $tree_id, $gedrec, |
731
|
|
|
)); |
732
|
|
|
// Update the cross-reference/index tables. |
733
|
|
|
self::updateLinks($xref, $tree_id, $gedrec); |
734
|
|
|
self::updateNames($xref, $tree_id, $record); |
735
|
|
|
break; |
736
|
|
|
case 'OBJE': |
737
|
|
|
$record = new Media($xref, $gedrec, null, $tree); |
738
|
|
|
Database::prepare( |
739
|
|
|
"INSERT INTO `##media` (m_id, m_ext, m_type, m_titl, m_filename, m_file, m_gedcom) VALUES (?, LEFT(?, 6), LEFT(?, 60), LEFT(?, 255), left(?, 512), ?, ?)" |
740
|
|
|
)->execute(array( |
741
|
|
|
$xref, $record->extension(), $record->getMediaType(), $record->getTitle(), $record->getFilename(), $tree_id, $gedrec, |
742
|
|
|
)); |
743
|
|
|
// Update the cross-reference/index tables. |
744
|
|
|
self::updateLinks($xref, $tree_id, $gedrec); |
745
|
|
|
self::updateNames($xref, $tree_id, $record); |
746
|
|
|
break; |
747
|
|
|
default: // HEAD, TRLR, SUBM, SUBN, and custom record types. |
748
|
|
|
// Force HEAD records to have a creation date. |
749
|
|
|
if ($type === 'HEAD' && strpos($gedrec, "\n1 DATE ") === false) { |
750
|
|
|
$gedrec .= "\n1 DATE " . date('j M Y'); |
751
|
|
|
} |
752
|
|
|
Database::prepare( |
753
|
|
|
"INSERT INTO `##other` (o_id, o_file, o_type, o_gedcom) VALUES (?, ?, LEFT(?, 15), ?)" |
754
|
|
|
)->execute(array($xref, $tree_id, $type, $gedrec)); |
755
|
|
|
// Update the cross-reference/index tables. |
756
|
|
|
self::updateLinks($xref, $tree_id, $gedrec); |
757
|
|
|
break; |
758
|
|
|
} |
759
|
|
|
} |
760
|
|
|
|
761
|
|
|
/** |
762
|
|
|
* extract all places from the given record and insert them into the places table |
763
|
|
|
* |
764
|
|
|
* @param string $gid |
765
|
|
|
* @param int $ged_id |
766
|
|
|
* @param string $gedrec |
767
|
|
|
*/ |
768
|
|
|
public static function updatePlaces($gid, $ged_id, $gedrec) |
769
|
|
|
{ |
770
|
|
|
global $placecache; |
771
|
|
|
|
772
|
|
|
if (!isset($placecache)) { |
773
|
|
|
$placecache = array(); |
774
|
|
|
} |
775
|
|
|
$personplace = array(); |
776
|
|
|
// import all place locations, but not control info such as |
777
|
|
|
// 0 HEAD/1 PLAC or 0 _EVDEF/1 PLAC |
778
|
|
|
$pt = preg_match_all("/^[2-9] PLAC (.+)/m", $gedrec, $match, PREG_SET_ORDER); |
779
|
|
|
for ($i = 0; $i < $pt; $i++) { |
780
|
|
|
$place = trim($match[$i][1]); |
781
|
|
|
$lowplace = I18N::strtolower($place); |
782
|
|
|
//-- if we have already visited this place for this person then we don't need to again |
783
|
|
|
if (isset($personplace[$lowplace])) { |
784
|
|
|
continue; |
785
|
|
|
} |
786
|
|
|
$personplace[$lowplace] = 1; |
787
|
|
|
$places = explode(',', $place); |
788
|
|
|
//-- reverse the array to start at the highest level |
789
|
|
|
$secalp = array_reverse($places); |
790
|
|
|
$parent_id = 0; |
791
|
|
|
$search = true; |
792
|
|
|
|
793
|
|
|
foreach ($secalp as $place) { |
794
|
|
|
$place = trim($place); |
795
|
|
|
$key = strtolower(mb_substr($place, 0, 150) . "_" . $parent_id); |
796
|
|
|
//-- if this place has already been added then we don't need to add it again |
797
|
|
|
if (isset($placecache[$key])) { |
798
|
|
|
$parent_id = $placecache[$key]; |
799
|
|
|
if (!isset($personplace[$key])) { |
800
|
|
|
$personplace[$key] = 1; |
801
|
|
|
// Use INSERT IGNORE as a (temporary) fix for https://bugs.launchpad.net/webtrees/+bug/582226 |
802
|
|
|
// It ignores places that utf8_unicode_ci consider to be the same (i.e. accents). |
803
|
|
|
// For example Québec and Quebec |
804
|
|
|
// We need a better solution that attaches multiple names to single places |
805
|
|
|
Database::prepare( |
806
|
|
|
"INSERT IGNORE INTO `##placelinks` (pl_p_id, pl_gid, pl_file) VALUES (?, ?, ?)" |
807
|
|
|
)->execute(array( |
808
|
|
|
$parent_id, $gid, $ged_id, |
809
|
|
|
)); |
810
|
|
|
} |
811
|
|
|
continue; |
812
|
|
|
} |
813
|
|
|
|
814
|
|
|
//-- only search the database while we are finding places in it |
815
|
|
|
if ($search) { |
816
|
|
|
//-- check if this place and level has already been added |
817
|
|
|
$tmp = Database::prepare( |
818
|
|
|
"SELECT p_id FROM `##places` WHERE p_file = ? AND p_parent_id = ? AND p_place = LEFT(?, 150)" |
819
|
|
|
)->execute(array( |
820
|
|
|
$ged_id, $parent_id, $place, |
821
|
|
|
))->fetchOne(); |
822
|
|
|
if ($tmp) { |
823
|
|
|
$p_id = $tmp; |
824
|
|
|
} else { |
825
|
|
|
$search = false; |
826
|
|
|
} |
827
|
|
|
} |
828
|
|
|
|
829
|
|
|
//-- if we are not searching then we have to insert the place into the db |
830
|
|
|
if (!$search) { |
831
|
|
|
$std_soundex = Soundex::russell($place); |
832
|
|
|
$dm_soundex = Soundex::daitchMokotoff($place); |
833
|
|
|
Database::prepare( |
834
|
|
|
"INSERT INTO `##places` (p_place, p_parent_id, p_file, p_std_soundex, p_dm_soundex) VALUES (LEFT(?, 150), ?, ?, ?, ?)" |
835
|
|
|
)->execute(array( |
836
|
|
|
$place, $parent_id, $ged_id, $std_soundex, $dm_soundex, |
837
|
|
|
)); |
838
|
|
|
$p_id = Database::getInstance()->lastInsertId(); |
839
|
|
|
} |
840
|
|
|
|
841
|
|
|
Database::prepare( |
842
|
|
|
"INSERT IGNORE INTO `##placelinks` (pl_p_id, pl_gid, pl_file) VALUES (?, ?, ?)" |
843
|
|
|
)->execute(array( |
844
|
|
|
$p_id, $gid, $ged_id, |
|
|
|
|
845
|
|
|
)); |
846
|
|
|
//-- increment the level and assign the parent id for the next place level |
847
|
|
|
$parent_id = $p_id; |
848
|
|
|
$placecache[$key] = $p_id; |
849
|
|
|
$personplace[$key] = 1; |
850
|
|
|
} |
851
|
|
|
} |
852
|
|
|
} |
853
|
|
|
|
854
|
|
|
/** |
855
|
|
|
* Extract all the dates from the given record and insert them into the database. |
856
|
|
|
* |
857
|
|
|
* @param string $xref |
858
|
|
|
* @param int $ged_id |
859
|
|
|
* @param string $gedrec |
860
|
|
|
*/ |
861
|
|
|
public static function updateDates($xref, $ged_id, $gedrec) |
862
|
|
|
{ |
863
|
|
|
if (strpos($gedrec, '2 DATE ') && preg_match_all("/\n1 (\w+).*(?:\n[2-9].*)*(?:\n2 DATE (.+))(?:\n[2-9].*)*/", $gedrec, $matches, PREG_SET_ORDER)) { |
864
|
|
|
foreach ($matches as $match) { |
865
|
|
|
$fact = $match[1]; |
866
|
|
|
if (($fact == 'FACT' || $fact == 'EVEN') && preg_match("/\n2 TYPE ([A-Z]{3,5})/", $match[0], $tmatch)) { |
867
|
|
|
$fact = $tmatch[1]; |
868
|
|
|
} |
869
|
|
|
$date = new Date($match[2]); |
870
|
|
|
Database::prepare( |
871
|
|
|
"INSERT INTO `##dates` (d_day,d_month,d_mon,d_year,d_julianday1,d_julianday2,d_fact,d_gid,d_file,d_type) VALUES (?,?,?,?,?,?,?,?,?,?)" |
872
|
|
|
)->execute(array( |
873
|
|
|
$date->minimumDate()->d, |
874
|
|
|
$date->minimumDate()->format('%O'), |
875
|
|
|
$date->minimumDate()->m, |
876
|
|
|
$date->minimumDate()->y, |
877
|
|
|
$date->minimumDate()->minJD, |
878
|
|
|
$date->minimumDate()->maxJD, |
879
|
|
|
$fact, |
880
|
|
|
$xref, |
881
|
|
|
$ged_id, |
882
|
|
|
$date->minimumDate()->format('%@'), |
883
|
|
|
)); |
884
|
|
|
if ($date->minimumDate() !== $date->maximumDate()) { |
885
|
|
|
Database::prepare( |
886
|
|
|
"INSERT INTO `##dates` (d_day,d_month,d_mon,d_year,d_julianday1,d_julianday2,d_fact,d_gid,d_file,d_type) VALUES (?,?,?,?,?,?,?,?,?,?)" |
887
|
|
|
)->execute(array( |
888
|
|
|
$date->maximumDate()->d, |
889
|
|
|
$date->maximumDate()->format('%O'), |
890
|
|
|
$date->maximumDate()->m, |
891
|
|
|
$date->maximumDate()->y, |
892
|
|
|
$date->maximumDate()->minJD, |
893
|
|
|
$date->maximumDate()->maxJD, |
894
|
|
|
$fact, |
895
|
|
|
$xref, |
896
|
|
|
$ged_id, |
897
|
|
|
$date->maximumDate()->format('%@'), |
898
|
|
|
)); |
899
|
|
|
} |
900
|
|
|
} |
901
|
|
|
} |
902
|
|
|
} |
903
|
|
|
|
904
|
|
|
/** |
905
|
|
|
* Extract all the links from the given record and insert them into the database |
906
|
|
|
* |
907
|
|
|
* @param string $xref |
908
|
|
|
* @param int $ged_id |
909
|
|
|
* @param string $gedrec |
910
|
|
|
*/ |
911
|
|
|
public static function updateLinks($xref, $ged_id, $gedrec) |
912
|
|
|
{ |
913
|
|
|
if (preg_match_all('/^\d+ (' . WT_REGEX_TAG . ') @(' . WT_REGEX_XREF . ')@/m', $gedrec, $matches, PREG_SET_ORDER)) { |
914
|
|
|
$data = array(); |
915
|
|
|
foreach ($matches as $match) { |
916
|
|
|
// Include each link once only. |
917
|
|
|
if (!in_array($match[1] . $match[2], $data)) { |
918
|
|
|
$data[] = $match[1] . $match[2]; |
919
|
|
|
// Ignore any errors, which may be caused by "duplicates" that differ on case/collation, e.g. "S1" and "s1" |
920
|
|
|
try { |
921
|
|
|
Database::prepare( |
922
|
|
|
"INSERT INTO `##link` (l_from, l_to, l_type, l_file) VALUES (?, ?, ?, ?)" |
923
|
|
|
)->execute(array( |
924
|
|
|
$xref, $match[2], $match[1], $ged_id, |
925
|
|
|
)); |
926
|
|
|
} catch (PDOException $e) { |
927
|
|
|
// We could display a warning here.... |
928
|
|
|
} |
929
|
|
|
} |
930
|
|
|
} |
931
|
|
|
} |
932
|
|
|
} |
933
|
|
|
|
934
|
|
|
/** |
935
|
|
|
* Extract all the names from the given record and insert them into the database. |
936
|
|
|
* |
937
|
|
|
* @param string $xref |
938
|
|
|
* @param int $ged_id |
939
|
|
|
* @param GedcomRecord $record |
940
|
|
|
*/ |
941
|
|
|
public static function updateNames($xref, $ged_id, GedcomRecord $record) |
942
|
|
|
{ |
943
|
|
|
foreach ($record->getAllNames() as $n => $name) { |
944
|
|
|
if ($record instanceof Individual) { |
945
|
|
|
if ($name['givn'] === '@P.N.') { |
946
|
|
|
$soundex_givn_std = null; |
947
|
|
|
$soundex_givn_dm = null; |
948
|
|
|
} else { |
949
|
|
|
$soundex_givn_std = Soundex::russell($name['givn']); |
950
|
|
|
$soundex_givn_dm = Soundex::daitchMokotoff($name['givn']); |
951
|
|
|
} |
952
|
|
|
if ($name['surn'] === '@N.N.') { |
953
|
|
|
$soundex_surn_std = null; |
954
|
|
|
$soundex_surn_dm = null; |
955
|
|
|
} else { |
956
|
|
|
$soundex_surn_std = Soundex::russell($name['surname']); |
957
|
|
|
$soundex_surn_dm = Soundex::daitchMokotoff($name['surname']); |
958
|
|
|
} |
959
|
|
|
Database::prepare( |
960
|
|
|
"INSERT INTO `##name` (n_file,n_id,n_num,n_type,n_sort,n_full,n_surname,n_surn,n_givn,n_soundex_givn_std,n_soundex_surn_std,n_soundex_givn_dm,n_soundex_surn_dm) VALUES (?, ?, ?, ?, LEFT(?, 255), LEFT(?, 255), LEFT(?, 255), LEFT(?, 255), ?, ?, ?, ?, ?)" |
961
|
|
|
)->execute(array( |
962
|
|
|
$ged_id, $xref, $n, $name['type'], $name['sort'], $name['fullNN'], $name['surname'], $name['surn'], $name['givn'], $soundex_givn_std, $soundex_surn_std, $soundex_givn_dm, $soundex_surn_dm, |
963
|
|
|
)); |
964
|
|
|
} else { |
965
|
|
|
Database::prepare( |
966
|
|
|
"INSERT INTO `##name` (n_file,n_id,n_num,n_type,n_sort,n_full) VALUES (?, ?, ?, ?, LEFT(?, 255), LEFT(?, 255))" |
967
|
|
|
)->execute(array( |
968
|
|
|
$ged_id, $xref, $n, $name['type'], $name['sort'], $name['fullNN'], |
969
|
|
|
)); |
970
|
|
|
} |
971
|
|
|
} |
972
|
|
|
} |
973
|
|
|
|
974
|
|
|
/** |
975
|
|
|
* Extract inline media data, and convert to media objects. |
976
|
|
|
* |
977
|
|
|
* @param Tree $tree |
978
|
|
|
* @param string $gedrec |
979
|
|
|
* |
980
|
|
|
* @return string |
981
|
|
|
*/ |
982
|
|
|
public static function convertInlineMedia(Tree $tree, $gedrec) |
983
|
|
|
{ |
984
|
|
|
while (preg_match('/\n1 OBJE(?:\n[2-9].+)+/', $gedrec, $match)) { |
985
|
|
|
$gedrec = str_replace($match[0], self::createMediaObject(1, $match[0], $tree), $gedrec); |
986
|
|
|
} |
987
|
|
|
while (preg_match('/\n2 OBJE(?:\n[3-9].+)+/', $gedrec, $match)) { |
988
|
|
|
$gedrec = str_replace($match[0], self::createMediaObject(2, $match[0], $tree), $gedrec); |
989
|
|
|
} |
990
|
|
|
while (preg_match('/\n3 OBJE(?:\n[4-9].+)+/', $gedrec, $match)) { |
991
|
|
|
$gedrec = str_replace($match[0], self::createMediaObject(3, $match[0], $tree), $gedrec); |
992
|
|
|
} |
993
|
|
|
|
994
|
|
|
return $gedrec; |
995
|
|
|
} |
996
|
|
|
|
997
|
|
|
/** |
998
|
|
|
* Create a new media object, from inline media data. |
999
|
|
|
* |
1000
|
|
|
* @param int $level |
1001
|
|
|
* @param string $gedrec |
1002
|
|
|
* @param Tree $tree |
1003
|
|
|
* |
1004
|
|
|
* @return string |
1005
|
|
|
*/ |
1006
|
|
|
public static function createMediaObject($level, $gedrec, Tree $tree) |
1007
|
|
|
{ |
1008
|
|
|
if (preg_match('/\n\d FILE (.+)/', $gedrec, $file_match)) { |
1009
|
|
|
$file = $file_match[1]; |
1010
|
|
|
} else { |
1011
|
|
|
$file = ''; |
1012
|
|
|
} |
1013
|
|
|
|
1014
|
|
|
if (preg_match('/\n\d TITL (.+)/', $gedrec, $file_match)) { |
1015
|
|
|
$titl = $file_match[1]; |
1016
|
|
|
} else { |
1017
|
|
|
$titl = ''; |
1018
|
|
|
} |
1019
|
|
|
|
1020
|
|
|
// Have we already created a media object with the same title/filename? |
1021
|
|
|
$xref = Database::prepare( |
1022
|
|
|
"SELECT m_id FROM `##media` WHERE m_filename = ? AND m_titl = ? AND m_file = ?" |
1023
|
|
|
)->execute(array( |
1024
|
|
|
$file, $titl, $tree->getTreeId(), |
1025
|
|
|
))->fetchOne(); |
1026
|
|
|
|
1027
|
|
|
if (!$xref) { |
1028
|
|
|
$xref = $tree->getNewXref('OBJE'); |
1029
|
|
|
// renumber the lines |
1030
|
|
|
$gedrec = preg_replace_callback('/\n(\d+)/', function ($m) use ($level) { |
1031
|
|
|
return "\n" . ($m[1] - $level); |
1032
|
|
|
}, $gedrec); |
1033
|
|
|
// convert to an object |
1034
|
|
|
$gedrec = str_replace("\n0 OBJE\n", '0 @' . $xref . "@ OBJE\n", $gedrec); |
1035
|
|
|
// Fix Legacy GEDCOMS |
1036
|
|
|
$gedrec = preg_replace('/\n1 FORM (.+)\n1 FILE (.+)\n1 TITL (.+)/', "\n1 FILE $2\n2 FORM $1\n2 TITL $3", $gedrec); |
1037
|
|
|
// Fix FTB GEDCOMS |
1038
|
|
|
$gedrec = preg_replace('/\n1 FORM (.+)\n1 TITL (.+)\n1 FILE (.+)/', "\n1 FILE $3\n2 FORM $1\n2 TITL $2", $gedrec); |
1039
|
|
|
// Create new record |
1040
|
|
|
$record = new Media($xref, $gedrec, null, $tree); |
1041
|
|
|
Database::prepare( |
1042
|
|
|
"INSERT INTO `##media` (m_id, m_ext, m_type, m_titl, m_filename, m_file, m_gedcom) VALUES (?, ?, ?, ?, ?, ?, ?)" |
1043
|
|
|
)->execute(array( |
1044
|
|
|
$xref, $record->extension(), $record->getMediaType(), $record->getTitle(), $record->getFilename(), $tree->getTreeId(), $gedrec, |
1045
|
|
|
)); |
1046
|
|
|
} |
1047
|
|
|
|
1048
|
|
|
return "\n" . $level . ' OBJE @' . $xref . '@'; |
1049
|
|
|
} |
1050
|
|
|
|
1051
|
|
|
/** |
1052
|
|
|
* Accept all pending changes for a specified record. |
1053
|
|
|
* |
1054
|
|
|
* @param string $xref |
1055
|
|
|
* @param int $ged_id |
1056
|
|
|
*/ |
1057
|
|
|
public static function acceptAllChanges($xref, $ged_id) |
1058
|
|
|
{ |
1059
|
|
|
$changes = Database::prepare( |
1060
|
|
|
"SELECT change_id, gedcom_name, old_gedcom, new_gedcom" . |
1061
|
|
|
" FROM `##change` c" . |
1062
|
|
|
" JOIN `##gedcom` g USING (gedcom_id)" . |
1063
|
|
|
" WHERE c.status='pending' AND xref=? AND gedcom_id=?" . |
1064
|
|
|
" ORDER BY change_id" |
1065
|
|
|
)->execute(array($xref, $ged_id))->fetchAll(); |
1066
|
|
|
foreach ($changes as $change) { |
1067
|
|
|
if (empty($change->new_gedcom)) { |
1068
|
|
|
// delete |
1069
|
|
|
self::updateRecord($change->old_gedcom, $ged_id, true); |
1070
|
|
|
} else { |
1071
|
|
|
// add/update |
1072
|
|
|
self::updateRecord($change->new_gedcom, $ged_id, false); |
1073
|
|
|
} |
1074
|
|
|
Database::prepare( |
1075
|
|
|
"UPDATE `##change`" . |
1076
|
|
|
" SET status='accepted'" . |
1077
|
|
|
" WHERE status='pending' AND xref=? AND gedcom_id=?" |
1078
|
|
|
)->execute(array($xref, $ged_id)); |
1079
|
|
|
Log::addEditLog("Accepted change {$change->change_id} for {$xref} / {$change->gedcom_name} into database"); |
1080
|
|
|
} |
1081
|
|
|
} |
1082
|
|
|
|
1083
|
|
|
/** |
1084
|
|
|
* Accept all pending changes for a specified record. |
1085
|
|
|
* |
1086
|
|
|
* @param GedcomRecord $record |
1087
|
|
|
*/ |
1088
|
|
|
public static function rejectAllChanges(GedcomRecord $record) |
1089
|
|
|
{ |
1090
|
|
|
Database::prepare( |
1091
|
|
|
"UPDATE `##change`" . |
1092
|
|
|
" SET status = 'rejected'" . |
1093
|
|
|
" WHERE status = 'pending' AND xref = :xref AND gedcom_id = :tree_id" |
1094
|
|
|
)->execute(array( |
1095
|
|
|
'xref' => $record->getXref(), |
1096
|
|
|
'tree_id' => $record->getTree()->getTreeId(), |
1097
|
|
|
)); |
1098
|
|
|
} |
1099
|
|
|
|
1100
|
|
|
/** |
1101
|
|
|
* update a record in the database |
1102
|
|
|
* |
1103
|
|
|
* @param string $gedrec |
1104
|
|
|
* @param int $ged_id |
1105
|
|
|
* @param bool $delete |
1106
|
|
|
*/ |
1107
|
|
|
public static function updateRecord($gedrec, $ged_id, $delete) |
1108
|
|
|
{ |
1109
|
|
|
if (preg_match('/^0 @(' . WT_REGEX_XREF . ')@ (' . WT_REGEX_TAG . ')/', $gedrec, $match)) { |
1110
|
|
|
list(, $gid, $type) = $match; |
1111
|
|
|
} elseif (preg_match('/^0 (HEAD)(?:\n|$)/', $gedrec, $match)) { |
1112
|
|
|
// The HEAD record has no XREF. Any others? |
1113
|
|
|
$gid = $match[1]; |
1114
|
|
|
$type = $match[1]; |
1115
|
|
|
} else { |
1116
|
|
|
echo "ERROR: Invalid gedcom record."; |
1117
|
|
|
|
1118
|
|
|
return; |
1119
|
|
|
} |
1120
|
|
|
|
1121
|
|
|
// TODO deleting unlinked places can be done more efficiently in a single query |
1122
|
|
|
$placeids = |
1123
|
|
|
Database::prepare("SELECT pl_p_id FROM `##placelinks` WHERE pl_gid=? AND pl_file=?") |
1124
|
|
|
->execute(array($gid, $ged_id)) |
1125
|
|
|
->fetchOneColumn(); |
1126
|
|
|
|
1127
|
|
|
Database::prepare("DELETE FROM `##placelinks` WHERE pl_gid=? AND pl_file=?")->execute(array($gid, $ged_id)); |
1128
|
|
|
Database::prepare("DELETE FROM `##dates` WHERE d_gid =? AND d_file =?")->execute(array($gid, $ged_id)); |
1129
|
|
|
|
1130
|
|
|
//-- delete any unlinked places |
1131
|
|
|
foreach ($placeids as $p_id) { |
1132
|
|
|
$num = |
1133
|
|
|
Database::prepare("SELECT count(pl_p_id) FROM `##placelinks` WHERE pl_p_id=? AND pl_file=?") |
1134
|
|
|
->execute(array($p_id, $ged_id)) |
1135
|
|
|
->fetchOne(); |
1136
|
|
|
if ($num == 0) { |
|
|
|
|
1137
|
|
|
Database::prepare("DELETE FROM `##places` WHERE p_id=? AND p_file=?")->execute(array($p_id, $ged_id)); |
1138
|
|
|
} |
1139
|
|
|
} |
1140
|
|
|
|
1141
|
|
|
Database::prepare("DELETE FROM `##name` WHERE n_id=? AND n_file=?")->execute(array($gid, $ged_id)); |
1142
|
|
|
Database::prepare("DELETE FROM `##link` WHERE l_from=? AND l_file=?")->execute(array($gid, $ged_id)); |
1143
|
|
|
|
1144
|
|
|
switch ($type) { |
1145
|
|
|
case 'INDI': |
1146
|
|
|
Database::prepare("DELETE FROM `##individuals` WHERE i_id=? AND i_file=?")->execute(array($gid, $ged_id)); |
1147
|
|
|
break; |
1148
|
|
|
case 'FAM': |
1149
|
|
|
Database::prepare("DELETE FROM `##families` WHERE f_id=? AND f_file=?")->execute(array($gid, $ged_id)); |
1150
|
|
|
break; |
1151
|
|
|
case 'SOUR': |
1152
|
|
|
Database::prepare("DELETE FROM `##sources` WHERE s_id=? AND s_file=?")->execute(array($gid, $ged_id)); |
1153
|
|
|
break; |
1154
|
|
|
case 'OBJE': |
1155
|
|
|
Database::prepare("DELETE FROM `##media` WHERE m_id=? AND m_file=?")->execute(array($gid, $ged_id)); |
1156
|
|
|
break; |
1157
|
|
|
default: |
1158
|
|
|
Database::prepare("DELETE FROM `##other` WHERE o_id=? AND o_file=?")->execute(array($gid, $ged_id)); |
1159
|
|
|
break; |
1160
|
|
|
} |
1161
|
|
|
|
1162
|
|
|
if (!$delete) { |
1163
|
|
|
self::importRecord($gedrec, Tree::findById($ged_id), true); |
1164
|
|
|
} |
1165
|
|
|
} |
1166
|
|
|
} |
1167
|
|
|
|