1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2021 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 <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees\Services; 21 22use Fisharebest\Webtrees\Gedcom; 23use Fisharebest\Webtrees\Registry; 24use Fisharebest\Webtrees\Tree; 25use Psr\Http\Message\ServerRequestInterface; 26 27use function array_merge; 28use function array_unique; 29use function assert; 30use function count; 31use function preg_match_all; 32use function str_replace; 33use function trim; 34 35/** 36 * Utilities to edit/save GEDCOM data. 37 */ 38class GedcomEditService 39{ 40 /** @var string[] */ 41 public $glevels = []; 42 43 /** @var string[] */ 44 public $tag = []; 45 46 /** @var string[] */ 47 public $islink = []; 48 49 /** @var string[] */ 50 public $text = []; 51 52 /** @var string[] */ 53 protected $glevelsSOUR = []; 54 55 /** @var string[] */ 56 protected $tagSOUR = []; 57 58 /** @var string[] */ 59 protected $islinkSOUR = []; 60 61 /** @var string[] */ 62 protected $textSOUR = []; 63 64 /** @var string[] */ 65 protected $glevelsRest = []; 66 67 /** @var string[] */ 68 protected $tagRest = []; 69 70 /** @var string[] */ 71 protected $islinkRest = []; 72 73 /** @var string[] */ 74 protected $textRest = []; 75 76 /** 77 * This function splits the $glevels, $tag, $islink, and $text arrays so that the 78 * entries associated with a SOUR record are separate from everything else. 79 * 80 * Input arrays: 81 * - $glevels[] - an array of the gedcom level for each line that was edited 82 * - $tag[] - an array of the tags for each gedcom line that was edited 83 * - $islink[] - an array of 1 or 0 values to indicate when the text is a link element 84 * - $text[] - an array of the text data for each line 85 * 86 * Output arrays: 87 * ** For the SOUR record: 88 * - $glevelsSOUR[] - an array of the gedcom level for each line that was edited 89 * - $tagSOUR[] - an array of the tags for each gedcom line that was edited 90 * - $islinkSOUR[] - an array of 1 or 0 values to indicate when the text is a link element 91 * - $textSOUR[] - an array of the text data for each line 92 * ** For the remaining records: 93 * - $glevelsRest[] - an array of the gedcom level for each line that was edited 94 * - $tagRest[] - an array of the tags for each gedcom line that was edited 95 * - $islinkRest[] - an array of 1 or 0 values to indicate when the text is a link element 96 * - $textRest[] - an array of the text data for each line 97 * 98 * @return void 99 */ 100 public function splitSource(): void 101 { 102 $this->glevelsSOUR = []; 103 $this->tagSOUR = []; 104 $this->islinkSOUR = []; 105 $this->textSOUR = []; 106 107 $this->glevelsRest = []; 108 $this->tagRest = []; 109 $this->islinkRest = []; 110 $this->textRest = []; 111 112 $inSOUR = false; 113 $levelSOUR = 0; 114 115 // Assume all arrays are the same size. 116 $count = count($this->glevels); 117 118 for ($i = 0; $i < $count; $i++) { 119 if ($inSOUR) { 120 if ($levelSOUR < $this->glevels[$i]) { 121 $dest = 'S'; 122 } else { 123 $inSOUR = false; 124 $dest = 'R'; 125 } 126 } elseif ($this->tag[$i] === 'SOUR') { 127 $inSOUR = true; 128 $levelSOUR = $this->glevels[$i]; 129 $dest = 'S'; 130 } else { 131 $dest = 'R'; 132 } 133 134 if ($dest === 'S') { 135 $this->glevelsSOUR[] = $this->glevels[$i]; 136 $this->tagSOUR[] = $this->tag[$i]; 137 $this->islinkSOUR[] = $this->islink[$i]; 138 $this->textSOUR[] = $this->text[$i]; 139 } else { 140 $this->glevelsRest[] = $this->glevels[$i]; 141 $this->tagRest[] = $this->tag[$i]; 142 $this->islinkRest[] = $this->islink[$i]; 143 $this->textRest[] = $this->text[$i]; 144 } 145 } 146 } 147 148 /** 149 * Add new GEDCOM lines from the $xxxRest interface update arrays, which 150 * were produced by the splitSOUR() function. 151 * See the FunctionsEdit::handle_updatesges() function for details. 152 * 153 * @param string $inputRec 154 * 155 * @return string 156 */ 157 public function updateRest(string $inputRec): string 158 { 159 if (count($this->tagRest) === 0) { 160 return $inputRec; // No update required 161 } 162 163 // Save original interface update arrays before replacing them with the xxxRest ones 164 $glevelsSave = $this->glevels; 165 $tagSave = $this->tag; 166 $islinkSave = $this->islink; 167 $textSave = $this->text; 168 169 $this->glevels = $this->glevelsRest; 170 $this->tag = $this->tagRest; 171 $this->islink = $this->islinkRest; 172 $this->text = $this->textRest; 173 174 $myRecord = $this->handleUpdates($inputRec, 'no'); // Now do the update 175 176 // Restore the original interface update arrays (just in case ...) 177 $this->glevels = $glevelsSave; 178 $this->tag = $tagSave; 179 $this->islink = $islinkSave; 180 $this->text = $textSave; 181 182 return $myRecord; 183 } 184 185 /** 186 * Add new gedcom lines from interface update arrays 187 * The edit_interface and FunctionsEdit::add_simple_tag function produce the following 188 * arrays incoming from the $_POST form 189 * - $glevels[] - an array of the gedcom level for each line that was edited 190 * - $tag[] - an array of the tags for each gedcom line that was edited 191 * - $islink[] - an array of 1 or 0 values to tell whether the text is a link element and should be surrounded by @@ 192 * - $text[] - an array of the text data for each line 193 * With these arrays you can recreate the gedcom lines like this 194 * <code>$glevel[0].' '.$tag[0].' '.$text[0]</code> 195 * There will be an index in each of these arrays for each line of the gedcom 196 * fact that is being edited. 197 * If the $text[] array is empty for the given line, then it means that the 198 * user removed that line during editing or that the line is supposed to be 199 * empty (1 DEAT, 1 BIRT) for example. To know if the line should be removed 200 * there is a section of code that looks ahead to the next lines to see if there 201 * are sub lines. For example we don't want to remove the 1 DEAT line if it has 202 * a 2 PLAC or 2 DATE line following it. If there are no sub lines, then the line 203 * can be safely removed. 204 * 205 * @param string $newged the new gedcom record to add the lines to 206 * @param string $levelOverride Override GEDCOM level specified in $glevels[0] 207 * 208 * @return string The updated gedcom record 209 */ 210 public function handleUpdates(string $newged, $levelOverride = 'no'): string 211 { 212 if ($levelOverride === 'no') { 213 $levelAdjust = 0; 214 } else { 215 $levelAdjust = 1; 216 } 217 218 // Assert all arrays are the same size. 219 assert(count($this->glevels) === count($this->tag)); 220 assert(count($this->glevels) === count($this->text)); 221 assert(count($this->glevels) === count($this->islink)); 222 223 $count = count($this->glevels); 224 225 for ($j = 0; $j < $count; $j++) { 226 // Look for empty SOUR reference with non-empty sub-records. 227 // This can happen when the SOUR entry is deleted but its sub-records 228 // were incorrectly left intact. 229 // The sub-records should be deleted. 230 if ($this->tag[$j] === 'SOUR' && ($this->text[$j] === '@@' || $this->text[$j] === '')) { 231 $this->text[$j] = ''; 232 $k = $j + 1; 233 while ($k < $count && $this->glevels[$k] > $this->glevels[$j]) { 234 $this->text[$k] = ''; 235 $k++; 236 } 237 } 238 239 if (trim($this->text[$j]) !== '') { 240 $pass = true; 241 } else { 242 //-- for facts with empty values they must have sub records 243 //-- this section checks if they have subrecords 244 $k = $j + 1; 245 $pass = false; 246 while ($k < $count && $this->glevels[$k] > $this->glevels[$j]) { 247 if ($this->text[$k] !== '') { 248 if ($this->tag[$j] !== 'OBJE' || $this->tag[$k] === 'FILE') { 249 $pass = true; 250 break; 251 } 252 } 253 $k++; 254 } 255 } 256 257 //-- if the value is not empty or it has sub lines 258 //--- then write the line to the gedcom record 259 //-- we have to let some emtpy text lines pass through... (DEAT, BIRT, etc) 260 if ($pass) { 261 $newline = (int) $this->glevels[$j] + $levelAdjust . ' ' . $this->tag[$j]; 262 if ($this->text[$j] !== '') { 263 if ($this->islink[$j]) { 264 $newline .= ' @' . $this->text[$j] . '@'; 265 } else { 266 $newline .= ' ' . $this->text[$j]; 267 } 268 } 269 $next_level = 1 + (int) $this->glevels[$j] + $levelAdjust; 270 271 $newged .= "\n" . str_replace("\n", "\n" . $next_level . ' CONT ', $newline); 272 } 273 } 274 275 return $newged; 276 } 277 278 /** 279 * Create a form to add a new fact. 280 * 281 * @param ServerRequestInterface $request 282 * @param Tree $tree 283 * @param string $fact 284 * 285 * @return string 286 */ 287 public function addNewFact(ServerRequestInterface $request, Tree $tree, string $fact): string 288 { 289 $params = (array) $request->getParsedBody(); 290 291 $FACT = $params[$fact]; 292 $DATE = $params[$fact . '_DATE'] ?? ''; 293 $PLAC = $params[$fact . '_PLAC'] ?? ''; 294 295 if ($DATE !== '' || $PLAC !== '' || $FACT !== '' && $FACT !== 'Y') { 296 if ($FACT !== '' && $FACT !== 'Y') { 297 $gedrec = "\n1 " . $fact . ' ' . $FACT; 298 } else { 299 $gedrec = "\n1 " . $fact; 300 } 301 if ($DATE !== '') { 302 $gedrec .= "\n2 DATE " . $DATE; 303 } 304 if ($PLAC !== '') { 305 $gedrec .= "\n2 PLAC " . $PLAC; 306 307 if (preg_match_all('/(' . Gedcom::REGEX_TAG . ')/', $tree->getPreference('ADVANCED_PLAC_FACTS'), $match)) { 308 foreach ($match[1] as $tag) { 309 $TAG = $params[$fact . '_' . $tag]; 310 if ($TAG !== '') { 311 $gedrec .= "\n3 " . $tag . ' ' . $TAG; 312 } 313 } 314 } 315 $LATI = $params[$fact . '_LATI'] ?? ''; 316 $LONG = $params[$fact . '_LONG'] ?? ''; 317 if ($LATI !== '' || $LONG !== '') { 318 $gedrec .= "\n3 MAP\n4 LATI " . $LATI . "\n4 LONG " . $LONG; 319 } 320 } 321 if ((bool) ($params['SOUR_' . $fact] ?? false)) { 322 return $this->updateSource($gedrec, 'yes'); 323 } 324 325 return $gedrec; 326 } 327 328 if ($FACT === 'Y') { 329 if ((bool) ($params['SOUR_' . $fact] ?? false)) { 330 return $this->updateSource("\n1 " . $fact . ' Y', 'yes'); 331 } 332 333 return "\n1 " . $fact . ' Y'; 334 } 335 336 return ''; 337 } 338 339 /** 340 * Add new GEDCOM lines from the $xxxSOUR interface update arrays, which 341 * were produced by the splitSOUR() function. 342 * See the FunctionsEdit::handle_updatesges() function for details. 343 * 344 * @param string $inputRec 345 * @param string $levelOverride 346 * 347 * @return string 348 */ 349 public function updateSource(string $inputRec, string $levelOverride = 'no'): string 350 { 351 if (count($this->tagSOUR) === 0) { 352 return $inputRec; // No update required 353 } 354 355 // Save original interface update arrays before replacing them with the xxxSOUR ones 356 $glevelsSave = $this->glevels; 357 $tagSave = $this->tag; 358 $islinkSave = $this->islink; 359 $textSave = $this->text; 360 361 $this->glevels = $this->glevelsSOUR; 362 $this->tag = $this->tagSOUR; 363 $this->islink = $this->islinkSOUR; 364 $this->text = $this->textSOUR; 365 366 $myRecord = $this->handleUpdates($inputRec, $levelOverride); // Now do the update 367 368 // Restore the original interface update arrays (just in case ...) 369 $this->glevels = $glevelsSave; 370 $this->tag = $tagSave; 371 $this->islink = $islinkSave; 372 $this->text = $textSave; 373 374 return $myRecord; 375 } 376 377 /** 378 * Create a form to add a sex record. 379 * 380 * @param ServerRequestInterface $request 381 * 382 * @return string 383 */ 384 public function addNewSex(ServerRequestInterface $request): string 385 { 386 $params = (array) $request->getParsedBody(); 387 388 switch ($params['SEX']) { 389 case 'M': 390 return "\n1 SEX M"; 391 case 'F': 392 return "\n1 SEX F"; 393 default: 394 return "\n1 SEX U"; 395 } 396 } 397 398 /** 399 * Assemble the pieces of a newly created record into gedcom 400 * 401 * @param ServerRequestInterface $request 402 * @param Tree $tree 403 * 404 * @return string 405 */ 406 public function addNewName(ServerRequestInterface $request, Tree $tree): string 407 { 408 $params = (array) $request->getParsedBody(); 409 $gedrec = "\n1 NAME " . $params['NAME']; 410 411 $tags = [ 412 'NPFX', 413 'GIVN', 414 'SPFX', 415 'SURN', 416 'NSFX', 417 'NICK', 418 ]; 419 420 if (preg_match_all('/(' . Gedcom::REGEX_TAG . ')/', $tree->getPreference('ADVANCED_NAME_FACTS'), $match)) { 421 $tags = array_merge($tags, $match[1]); 422 } 423 424 // Paternal and Polish and Lithuanian surname traditions can also create a _MARNM 425 $SURNAME_TRADITION = $tree->getPreference('SURNAME_TRADITION'); 426 if ($SURNAME_TRADITION === 'paternal' || $SURNAME_TRADITION === 'polish' || $SURNAME_TRADITION === 'lithuanian') { 427 $tags[] = '_MARNM'; 428 } 429 430 foreach (array_unique($tags) as $tag) { 431 $TAG = $params[$tag]; 432 433 if ($TAG !== '') { 434 $gedrec .= "\n2 " . $tag . ' ' . $TAG; 435 } 436 } 437 438 return $gedrec; 439 } 440 441 /** 442 * Reassemble edited GEDCOM fields into a GEDCOM fact/event string. 443 * 444 * @param string $record_type 445 * @param array<string> $levels 446 * @param array<string> $tags 447 * @param array<string> $values 448 * 449 * @return string 450 */ 451 public function editLinesToGedcom(string $record_type, array $levels, array $tags, array $values): string 452 { 453 // Assert all arrays are the same size. 454 $count = count($levels); 455 assert($count > 0); 456 assert(count($tags) === $count); 457 assert(count($values) === $count); 458 459 $gedcom_lines = []; 460 $hierarchy = [$record_type]; 461 462 for ($i = 0; $i < $count; $i++) { 463 $hierarchy[$levels[$i]] = $tags[$i]; 464 465 $full_tag = implode(':', array_slice($hierarchy, 0, 1 + (int) $levels[$i])); 466 $element = Registry::elementFactory()->make($full_tag); 467 $values[$i] = $element->canonical($values[$i]); 468 469 // If "1 FACT Y" has a DATE or PLAC, then delete the value of Y 470 if ($levels[$i] === '1' && $values[$i] === 'Y') { 471 for ($j = $i + 1; $j < $count && $levels[$j] > $levels[$i]; ++$j) { 472 if ($levels[$j] === '2' && ($tags[$j] === 'DATE' || $tags[$j] === 'PLAC') && $values[$j] !== '') { 473 $values[$i] = ''; 474 break; 475 } 476 } 477 } 478 479 // Include this line if there is a value - or if there is a child record with a value. 480 $include = $values[$i] !== ''; 481 482 for ($j = $i + 1; !$include && $j < $count && $levels[$j] > $levels[$i]; $j++) { 483 $include = $values[$j] !== ''; 484 } 485 486 if ($include) { 487 if ($values[$i] === '') { 488 $gedcom_lines[] = $levels[$i] . ' ' . $tags[$i]; 489 } else { 490 $next_level = 1 + (int) $levels[$i]; 491 492 $gedcom_lines[] = $levels[$i] . ' ' . $tags[$i] . ' ' . str_replace("\n", "\n" . $next_level . ' CONT ', $values[$i]); 493 } 494 } 495 } 496 497 return implode("\n", $gedcom_lines); 498 } 499} 500