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