xref: /webtrees/app/Http/RequestHandlers/SearchReplaceAction.php (revision e218f363523a4bc74e0e636c21220b0954cff34d)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2019 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 <http://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 InvalidArgumentException;
29use Psr\Http\Message\ResponseInterface;
30use Psr\Http\Message\ServerRequestInterface;
31use Psr\Http\Server\RequestHandlerInterface;
32
33use function assert;
34
35/**
36 * Search and replace genealogy data
37 */
38class SearchReplaceAction implements RequestHandlerInterface
39{
40    use ViewResponseTrait;
41
42    /** @var SearchService */
43    private $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, new InvalidArgumentException());
66
67        $params  = $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                $adv_name_tags = preg_split("/[\s,;: ]+/", $tree->getPreference('ADVANCED_NAME_FACTS'));
97                $name_tags     = array_unique(array_merge([
98                    'NAME',
99                    'NPFX',
100                    'GIVN',
101                    'SPFX',
102                    'SURN',
103                    'NSFX',
104                    '_MARNM',
105                    '_AKA',
106                ], $adv_name_tags));
107
108                $records = $this->search_service->searchIndividuals([$tree], [$search]);
109                $count   = $this->replaceIndividualNames($records, $search, $replace, $name_tags);
110                FlashMessages::addMessage(I18N::plural('%s individual has been updated.', '%s individuals have been updated.', $count, I18N::number($count)));
111                break;
112
113            case 'place':
114                $records = $this->search_service->searchIndividuals([$tree], [$search]);
115                $count   = $this->replacePlaces($records, $search, $replace);
116                FlashMessages::addMessage(I18N::plural('%s individual has been updated.', '%s individuals have been updated.', $count, I18N::number($count)));
117
118                $records = $this->search_service->searchFamilies([$tree], [$search]);
119                $count   = $this->replacePlaces($records, $search, $replace);
120                FlashMessages::addMessage(I18N::plural('%s family has been updated.', '%s families have been updated.', $count, I18N::number($count)));
121                break;
122        }
123
124        $url = route(SearchReplacePage::class, [
125            'search'  => $search,
126            'replace' => $replace,
127            'context' => $context,
128            'tree'    => $tree->name(),
129        ]);
130
131        return redirect($url);
132    }
133
134    /**
135     * @param Collection $records
136     * @param string     $search
137     * @param string     $replace
138     *
139     * @return int
140     */
141    private function replaceRecords(Collection $records, string $search, string $replace): int
142    {
143        $count = 0;
144        $query = preg_quote($search, '/');
145
146        foreach ($records as $record) {
147            $old_record = $record->gedcom();
148            $new_record = preg_replace('/(\n\d [A-Z0-9_]+ )' . $query . '/i', '$1' . $replace, $old_record);
149
150            if ($new_record !== $old_record) {
151                $record->updateRecord($new_record, true);
152                $count++;
153            }
154        }
155
156        return $count;
157    }
158
159    /**
160     * @param Collection $records
161     * @param string     $search
162     * @param string     $replace
163     * @param string[]   $name_tags
164     *
165     * @return int
166     */
167    private function replaceIndividualNames(Collection $records, string $search, string $replace, array $name_tags): int
168    {
169        $pattern     = '/(\n\d (?:' . implode('|', $name_tags) . ') (?:.*))' . preg_quote($search, '/') . '/i';
170        $replacement = '$1' . $replace;
171        $count       = 0;
172
173        foreach ($records as $record) {
174            $old_gedcom = $record->gedcom();
175            $new_gedcom = preg_replace($pattern, $replacement, $old_gedcom);
176
177            if ($new_gedcom !== $old_gedcom) {
178                $record->updateRecord($new_gedcom, true);
179                $count++;
180            }
181        }
182
183        return $count;
184    }
185
186    /**
187     * @param Collection $records
188     * @param string     $search
189     * @param string     $replace
190     *
191     * @return int
192     */
193    private function replacePlaces(Collection $records, string $search, string $replace): int
194    {
195        $pattern     = '/(\n\d PLAC\b.* )' . preg_quote($search, '/') . '([,\n])/i';
196        $replacement = '$1' . $replace . '$2';
197        $count       = 0;
198
199        foreach ($records as $record) {
200            $old_gedcom = $record->gedcom();
201            $new_gedcom = preg_replace($pattern, $replacement, $old_gedcom);
202
203            if ($new_gedcom !== $old_gedcom) {
204                $record->updateRecord($new_gedcom, true);
205                $count++;
206            }
207        }
208
209        return $count;
210    }
211}
212