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