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