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