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