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