xref: /webtrees/app/Http/RequestHandlers/SearchGeneralPage.php (revision 5bfc689774bb9a6401271c4ed15a6d50652c991b)
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\Family;
23use Fisharebest\Webtrees\Http\ViewResponseTrait;
24use Fisharebest\Webtrees\I18N;
25use Fisharebest\Webtrees\Location;
26use Fisharebest\Webtrees\Note;
27use Fisharebest\Webtrees\Repository;
28use Fisharebest\Webtrees\Services\SearchService;
29use Fisharebest\Webtrees\Services\TreeService;
30use Fisharebest\Webtrees\Site;
31use Fisharebest\Webtrees\Tree;
32use Fisharebest\Webtrees\Validator;
33use Illuminate\Database\Capsule\Manager as DB;
34use Illuminate\Support\Collection;
35use Psr\Http\Message\ResponseInterface;
36use Psr\Http\Message\ServerRequestInterface;
37use Psr\Http\Server\RequestHandlerInterface;
38
39use function preg_replace;
40use function redirect;
41use function trim;
42
43use const PREG_SET_ORDER;
44
45/**
46 * Search for genealogy data
47 */
48class SearchGeneralPage implements RequestHandlerInterface
49{
50    use ViewResponseTrait;
51
52    private SearchService $search_service;
53
54    private TreeService $tree_service;
55
56    /**
57     * SearchController constructor.
58     *
59     * @param SearchService $search_service
60     * @param TreeService   $tree_service
61     */
62    public function __construct(SearchService $search_service, TreeService $tree_service)
63    {
64        $this->search_service = $search_service;
65        $this->tree_service   = $tree_service;
66    }
67
68    /**
69     * The standard search.
70     *
71     * @param ServerRequestInterface $request
72     *
73     * @return ResponseInterface
74     */
75    public function handle(ServerRequestInterface $request): ResponseInterface
76    {
77        $tree = Validator::attributes($request)->tree();
78
79        $params = $request->getQueryParams();
80        $query  = $params['query'] ?? '';
81
82        // What type of records to search?
83        $search_individuals  = (bool) ($params['search_individuals'] ?? false);
84        $search_families     = (bool) ($params['search_families'] ?? false);
85        $search_locations    = (bool) ($params['search_locations'] ?? false);
86        $search_repositories = (bool) ($params['search_repositories'] ?? false);
87        $search_sources      = (bool) ($params['search_sources'] ?? false);
88        $search_notes        = (bool) ($params['search_notes'] ?? false);
89
90        $exist_notes = DB::table('other')
91            ->where('o_file', '=', $tree->id())
92            ->where('o_type', '=', Note::RECORD_TYPE)
93            ->exists();
94
95        $exist_locations = DB::table('other')
96            ->where('o_file', '=', $tree->id())
97            ->where('o_type', '=', Location::RECORD_TYPE)
98            ->exists();
99
100        $exist_repositories = DB::table('other')
101            ->where('o_file', '=', $tree->id())
102            ->where('o_type', '=', Repository::RECORD_TYPE)
103            ->exists();
104
105        $exist_sources = DB::table('sources')
106            ->where('s_file', '=', $tree->id())
107            ->exists();
108
109        // Default to families and individuals only
110        if (!$search_individuals && !$search_families && !$search_repositories && !$search_sources && !$search_notes) {
111            $search_families    = true;
112            $search_individuals = true;
113        }
114
115        // What to search for?
116        $search_terms = $this->extractSearchTerms($query);
117
118        // What trees to search?
119        if (Site::getPreference('ALLOW_CHANGE_GEDCOM') === '1') {
120            $all_trees = $this->tree_service->all();
121        } else {
122            $all_trees = new Collection([$tree]);
123        }
124
125        $search_tree_names = new Collection($params['search_trees'] ?? []);
126
127        $search_trees = $all_trees
128            ->filter(static function (Tree $tree) use ($search_tree_names): bool {
129                return $search_tree_names->containsStrict($tree->name());
130            });
131
132        if ($search_trees->isEmpty()) {
133            $search_trees->add($tree);
134        }
135
136        // Do the search
137        $individuals  = new Collection();
138        $families     = new Collection();
139        $locations    = new Collection();
140        $repositories = new Collection();
141        $sources      = new Collection();
142        $notes        = new Collection();
143
144        if ($search_terms !== []) {
145            if ($search_individuals) {
146                $individuals = $this->search_service->searchIndividuals($search_trees->all(), $search_terms);
147            }
148
149            if ($search_families) {
150                $tmp1 = $this->search_service->searchFamilies($search_trees->all(), $search_terms);
151                $tmp2 = $this->search_service->searchFamilyNames($search_trees->all(), $search_terms);
152
153                $families = $tmp1->merge($tmp2)->unique(static function (Family $family): string {
154                    return $family->xref() . '@' . $family->tree()->id();
155                });
156            }
157
158            if ($search_repositories) {
159                $repositories = $this->search_service->searchRepositories($search_trees->all(), $search_terms);
160            }
161
162            if ($search_sources) {
163                $sources = $this->search_service->searchSources($search_trees->all(), $search_terms);
164            }
165
166            if ($search_notes) {
167                $notes = $this->search_service->searchNotes($search_trees->all(), $search_terms);
168            }
169
170            if ($search_locations) {
171                $locations = $this->search_service->searchLocations($search_trees->all(), $search_terms);
172            }
173        }
174
175        // If only 1 item is returned, automatically forward to that item
176        if ($individuals->count() === 1 && $families->isEmpty() && $sources->isEmpty() && $notes->isEmpty() && $locations->isEmpty()) {
177            return redirect($individuals->first()->url());
178        }
179
180        if ($individuals->isEmpty() && $families->count() === 1 && $sources->isEmpty() && $notes->isEmpty() && $locations->isEmpty()) {
181            return redirect($families->first()->url());
182        }
183
184        if ($individuals->isEmpty() && $families->isEmpty() && $sources->count() === 1 && $notes->isEmpty() && $locations->isEmpty()) {
185            return redirect($sources->first()->url());
186        }
187
188        if ($individuals->isEmpty() && $families->isEmpty() && $sources->isEmpty() && $notes->count() === 1 && $locations->isEmpty()) {
189            return redirect($notes->first()->url());
190        }
191
192        if ($individuals->isEmpty() && $families->isEmpty() && $sources->isEmpty() && $notes->isEmpty() && $locations->count() === 1) {
193            return redirect($locations->first()->url());
194        }
195
196        $title = I18N::translate('General search');
197
198        return $this->viewResponse('search-general-page', [
199            'all_trees'           => $all_trees,
200            'exist_locations'     => $exist_locations,
201            'exist_notes'         => $exist_notes,
202            'exist_repositories'  => $exist_repositories,
203            'exist_sources'       => $exist_sources,
204            'families'            => $families,
205            'individuals'         => $individuals,
206            'locations'           => $locations,
207            'notes'               => $notes,
208            'query'               => $query,
209            'repositories'        => $repositories,
210            'search_families'     => $search_families,
211            'search_individuals'  => $search_individuals,
212            'search_locations'    => $search_locations,
213            'search_notes'        => $search_notes,
214            'search_repositories' => $search_repositories,
215            'search_sources'      => $search_sources,
216            'search_trees'        => $search_trees,
217            'sources'             => $sources,
218            'title'               => $title,
219            'tree'                => $tree,
220        ]);
221    }
222
223    /**
224     * Convert the query into an array of search terms
225     *
226     * @param string $query
227     *
228     * @return array<string>
229     */
230    private function extractSearchTerms(string $query): array
231    {
232        $search_terms = [];
233
234        // Words in double quotes stay together
235        preg_match_all('/"([^"]+)"/', $query, $matches, PREG_SET_ORDER);
236        foreach ($matches as $match) {
237            $search_terms[] = trim($match[1]);
238            // Remove this string from the search query
239            $query = strtr($query, [$match[0] => '']);
240        }
241
242        // Treat CJK characters as separate words, not as characters.
243        $query = preg_replace('/\p{Han}/u', '$0 ', $query);
244
245        // Other words get treated separately
246        preg_match_all('/[\S]+/', $query, $matches, PREG_SET_ORDER);
247        foreach ($matches as $match) {
248            $search_terms[] = $match[0];
249        }
250
251        return $search_terms;
252    }
253}
254