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