xref: /webtrees/app/Http/RequestHandlers/SearchReplaceAction.php (revision a8b39ba2d349525974d59077419f7a8ea8802aaa)
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(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<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<GedcomRecord> $records
153     * @param string                   $search
154     * @param string                   $replace
155     * @param 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<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