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