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