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\Registry; 26use Fisharebest\Webtrees\Services\SearchService; 27use Fisharebest\Webtrees\Tree; 28use Illuminate\Support\Collection; 29use Psr\Http\Message\ResponseInterface; 30use Psr\Http\Message\ServerRequestInterface; 31use Psr\Http\Server\RequestHandlerInterface; 32 33use function assert; 34 35/** 36 * Search and replace genealogy data 37 */ 38class SearchReplaceAction implements RequestHandlerInterface 39{ 40 use ViewResponseTrait; 41 42 private SearchService $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 $name_tags = Registry::elementFactory()->make('INDI:NAME')->subtags(); 96 $name_tags = array_map(fn (string $tag): string => '2 ' . $tag, $name_tags); 97 $name_tags[] = '1 NAME'; 98 99 $records = $this->search_service->searchIndividuals([$tree], [$search]); 100 $count = $this->replaceIndividualNames($records, $search, $replace, $name_tags); 101 FlashMessages::addMessage(I18N::plural('%s individual has been updated.', '%s individuals have been updated.', $count, I18N::number($count))); 102 break; 103 104 case 'place': 105 $records = $this->search_service->searchIndividuals([$tree], [$search]); 106 $count = $this->replacePlaces($records, $search, $replace); 107 FlashMessages::addMessage(I18N::plural('%s individual has been updated.', '%s individuals have been updated.', $count, I18N::number($count))); 108 109 $records = $this->search_service->searchFamilies([$tree], [$search]); 110 $count = $this->replacePlaces($records, $search, $replace); 111 FlashMessages::addMessage(I18N::plural('%s family has been updated.', '%s families have been updated.', $count, I18N::number($count))); 112 break; 113 } 114 115 $url = route(SearchReplacePage::class, [ 116 'search' => $search, 117 'replace' => $replace, 118 'context' => $context, 119 'tree' => $tree->name(), 120 ]); 121 122 return redirect($url); 123 } 124 125 /** 126 * @param Collection $records 127 * @param string $search 128 * @param string $replace 129 * 130 * @return int 131 */ 132 private function replaceRecords(Collection $records, string $search, string $replace): int 133 { 134 $count = 0; 135 $query = preg_quote($search, '/'); 136 137 foreach ($records as $record) { 138 $old_record = $record->gedcom(); 139 $new_record = preg_replace('/(\n\d [A-Z0-9_]+ )' . $query . '/i', '$1' . $replace, $old_record); 140 141 if ($new_record !== $old_record) { 142 $record->updateRecord($new_record, true); 143 $count++; 144 } 145 } 146 147 return $count; 148 } 149 150 /** 151 * @param Collection $records 152 * @param string $search 153 * @param string $replace 154 * @param string[] $name_tags 155 * 156 * @return int 157 */ 158 private function replaceIndividualNames(Collection $records, string $search, string $replace, array $name_tags): int 159 { 160 $pattern = '/(\n(?:' . implode('|', $name_tags) . ') (?:.*))' . preg_quote($search, '/') . '/i'; 161 $replacement = '$1' . $replace; 162 $count = 0; 163 164 foreach ($records as $record) { 165 $old_gedcom = $record->gedcom(); 166 $new_gedcom = preg_replace($pattern, $replacement, $old_gedcom); 167 168 if ($new_gedcom !== $old_gedcom) { 169 $record->updateRecord($new_gedcom, true); 170 $count++; 171 } 172 } 173 174 return $count; 175 } 176 177 /** 178 * @param Collection $records 179 * @param string $search 180 * @param string $replace 181 * 182 * @return int 183 */ 184 private function replacePlaces(Collection $records, string $search, string $replace): int 185 { 186 $pattern = '/(\n\d PLAC\b.* )' . preg_quote($search, '/') . '([,\n])/i'; 187 $replacement = '$1' . $replace . '$2'; 188 $count = 0; 189 190 foreach ($records as $record) { 191 $old_gedcom = $record->gedcom(); 192 $new_gedcom = preg_replace($pattern, $replacement, $old_gedcom); 193 194 if ($new_gedcom !== $old_gedcom) { 195 $record->updateRecord($new_gedcom, true); 196 $count++; 197 } 198 } 199 200 return $count; 201 } 202} 203