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