1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 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\Http\RequestHandlers; 21 22use Fisharebest\Webtrees\FlashMessages; 23use Fisharebest\Webtrees\GedcomRecord; 24use Fisharebest\Webtrees\Http\ViewResponseTrait; 25use Fisharebest\Webtrees\I18N; 26use Fisharebest\Webtrees\Registry; 27use Fisharebest\Webtrees\Services\SearchService; 28use Fisharebest\Webtrees\Validator; 29use Illuminate\Support\Collection; 30use Psr\Http\Message\ResponseInterface; 31use Psr\Http\Message\ServerRequestInterface; 32use Psr\Http\Server\RequestHandlerInterface; 33 34/** 35 * Search and replace genealogy data 36 */ 37class SearchReplaceAction implements RequestHandlerInterface 38{ 39 use ViewResponseTrait; 40 41 private SearchService $search_service; 42 43 /** 44 * @param SearchService $search_service 45 */ 46 public function __construct(SearchService $search_service) 47 { 48 $this->search_service = $search_service; 49 } 50 51 /** 52 * Search and replace. 53 * 54 * @param ServerRequestInterface $request 55 * 56 * @return ResponseInterface 57 */ 58 public function handle(ServerRequestInterface $request): ResponseInterface 59 { 60 $tree = Validator::attributes($request)->tree(); 61 $search = Validator::parsedBody($request)->string('search'); 62 $replace = Validator::parsedBody($request)->string('replace'); 63 $context = Validator::parsedBody($request)->string('context'); 64 65 switch ($context) { 66 case 'all': 67 $records = $this->search_service->searchIndividuals([$tree], [$search]); 68 $count = $this->replaceRecords($records, $search, $replace); 69 FlashMessages::addMessage(I18N::plural('%s individual has been updated.', '%s individuals have been updated.', $count, I18N::number($count))); 70 71 $records = $this->search_service->searchFamilies([$tree], [$search]); 72 $count = $this->replaceRecords($records, $search, $replace); 73 FlashMessages::addMessage(I18N::plural('%s family has been updated.', '%s families have been updated.', $count, I18N::number($count))); 74 75 $records = $this->search_service->searchRepositories([$tree], [$search]); 76 $count = $this->replaceRecords($records, $search, $replace); 77 FlashMessages::addMessage(I18N::plural('%s repository has been updated.', '%s repositories have been updated.', $count, I18N::number($count))); 78 79 $records = $this->search_service->searchSources([$tree], [$search]); 80 $count = $this->replaceRecords($records, $search, $replace); 81 FlashMessages::addMessage(I18N::plural('%s source has been updated.', '%s sources have been updated.', $count, I18N::number($count))); 82 83 $records = $this->search_service->searchNotes([$tree], [$search]); 84 $count = $this->replaceRecords($records, $search, $replace); 85 FlashMessages::addMessage(I18N::plural('%s note has been updated.', '%s notes have been updated.', $count, I18N::number($count))); 86 break; 87 88 case 'name': 89 $name_tags = Registry::elementFactory()->make('INDI:NAME')->subtags(); 90 $name_tags = array_map(static fn (string $tag): string => '2 ' . $tag, $name_tags); 91 $name_tags[] = '1 NAME'; 92 93 $records = $this->search_service->searchIndividuals([$tree], [$search]); 94 $count = $this->replaceIndividualNames($records, $search, $replace, $name_tags); 95 FlashMessages::addMessage(I18N::plural('%s individual has been updated.', '%s individuals have been updated.', $count, I18N::number($count))); 96 break; 97 98 case 'place': 99 $records = $this->search_service->searchIndividuals([$tree], [$search]); 100 $count = $this->replacePlaces($records, $search, $replace); 101 FlashMessages::addMessage(I18N::plural('%s individual has been updated.', '%s individuals have been updated.', $count, I18N::number($count))); 102 103 $records = $this->search_service->searchFamilies([$tree], [$search]); 104 $count = $this->replacePlaces($records, $search, $replace); 105 FlashMessages::addMessage(I18N::plural('%s family has been updated.', '%s families have been updated.', $count, I18N::number($count))); 106 break; 107 } 108 109 $url = route(SearchReplacePage::class, [ 110 'search' => $search, 111 'replace' => $replace, 112 'context' => $context, 113 'tree' => $tree->name(), 114 ]); 115 116 return redirect($url); 117 } 118 119 /** 120 * @param Collection<int,GedcomRecord> $records 121 * @param string $search 122 * @param string $replace 123 * 124 * @return int 125 */ 126 private function replaceRecords(Collection $records, string $search, string $replace): int 127 { 128 $count = 0; 129 $query = preg_quote($search, '/'); 130 131 foreach ($records as $record) { 132 $old_record = $record->gedcom(); 133 $new_record = preg_replace('/(\n\d [A-Z0-9_]+ )' . $query . '/i', '$1' . $replace, $old_record); 134 135 if ($new_record !== $old_record) { 136 $record->updateRecord($new_record, true); 137 $count++; 138 } 139 } 140 141 return $count; 142 } 143 144 /** 145 * @param Collection<int,GedcomRecord> $records 146 * @param string $search 147 * @param string $replace 148 * @param array<string> $name_tags 149 * 150 * @return int 151 */ 152 private function replaceIndividualNames(Collection $records, string $search, string $replace, array $name_tags): int 153 { 154 $pattern = '/(\n(?:' . implode('|', $name_tags) . ') .*)' . preg_quote($search, '/') . '/i'; 155 $replacement = '$1' . $replace; 156 $count = 0; 157 158 foreach ($records as $record) { 159 $old_gedcom = $record->gedcom(); 160 $new_gedcom = preg_replace($pattern, $replacement, $old_gedcom); 161 162 if ($new_gedcom !== $old_gedcom) { 163 $record->updateRecord($new_gedcom, true); 164 $count++; 165 } 166 } 167 168 return $count; 169 } 170 171 /** 172 * @param Collection<int,GedcomRecord> $records 173 * @param string $search 174 * @param string $replace 175 * 176 * @return int 177 */ 178 private function replacePlaces(Collection $records, string $search, string $replace): int 179 { 180 $pattern = '/(\n\d PLAC\b.* )' . preg_quote($search, '/') . '([,\n])/i'; 181 $replacement = '$1' . $replace . '$2'; 182 $count = 0; 183 184 foreach ($records as $record) { 185 $old_gedcom = $record->gedcom(); 186 $new_gedcom = preg_replace($pattern, $replacement, $old_gedcom); 187 188 if ($new_gedcom !== $old_gedcom) { 189 $record->updateRecord($new_gedcom, true); 190 $count++; 191 } 192 } 193 194 return $count; 195 } 196} 197