SearchGeneralPage::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 2
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\RequestHandlers;
21
22
use Fisharebest\Webtrees\Auth;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Auth was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
23
use Fisharebest\Webtrees\DB;
24
use Fisharebest\Webtrees\Family;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Family was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
25
use Fisharebest\Webtrees\Http\ViewResponseTrait;
26
use Fisharebest\Webtrees\I18N;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\I18N was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
use Fisharebest\Webtrees\Location;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Location was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
28
use Fisharebest\Webtrees\Log;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Log was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
29
use Fisharebest\Webtrees\Note;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Note was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
30
use Fisharebest\Webtrees\Repository;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Repository was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
31
use Fisharebest\Webtrees\Services\SearchService;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Services\SearchService was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
32
use Fisharebest\Webtrees\Services\TreeService;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Services\TreeService was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
33
use Fisharebest\Webtrees\Site;
34
use Fisharebest\Webtrees\Tree;
35
use Fisharebest\Webtrees\Validator;
36
use Illuminate\Support\Collection;
37
use Psr\Http\Message\ResponseInterface;
38
use Psr\Http\Message\ServerRequestInterface;
39
use Psr\Http\Server\RequestHandlerInterface;
40
41
use function in_array;
42
use function preg_replace;
43
use function redirect;
44
use function trim;
45
46
use const PREG_SET_ORDER;
47
48
final class SearchGeneralPage implements RequestHandlerInterface
49
{
50
    use ViewResponseTrait;
51
52
    public function __construct(
53
        private readonly SearchService $search_service,
54
        private readonly TreeService $tree_service,
55
    ) {
56
    }
57
58
    public function handle(ServerRequestInterface $request): ResponseInterface
59
    {
60
        $tree = Validator::attributes($request)->tree();
61
62
        $query = Validator::queryParams($request)->string('query', '');
63
64
        // What type of records to search?
65
        $search_individuals  = Validator::queryParams($request)->boolean('search_individuals', false);
66
        $search_families     = Validator::queryParams($request)->boolean('search_families', false);
67
        $search_locations    = Validator::queryParams($request)->boolean('search_locations', false);
68
        $search_repositories = Validator::queryParams($request)->boolean('search_repositories', false);
69
        $search_sources      = Validator::queryParams($request)->boolean('search_sources', false);
70
        $search_notes        = Validator::queryParams($request)->boolean('search_notes', false);
71
72
        // Where to search
73
        $search_tree_names = Validator::queryParams($request)->array('search_trees');
74
75
        $exist_notes = DB::table('other')
76
            ->where('o_file', '=', $tree->id())
77
            ->where('o_type', '=', Note::RECORD_TYPE)
78
            ->exists();
79
80
        $exist_locations = DB::table('other')
81
            ->where('o_file', '=', $tree->id())
82
            ->where('o_type', '=', Location::RECORD_TYPE)
83
            ->exists();
84
85
        $exist_repositories = DB::table('other')
86
            ->where('o_file', '=', $tree->id())
87
            ->where('o_type', '=', Repository::RECORD_TYPE)
88
            ->exists();
89
90
        $exist_sources = DB::table('sources')
91
            ->where('s_file', '=', $tree->id())
92
            ->exists();
93
94
        // Default to families and individuals only
95
        if (!$search_individuals && !$search_families && !$search_repositories && !$search_sources && !$search_notes) {
96
            $search_families    = true;
97
            $search_individuals = true;
98
        }
99
100
        // What to search for?
101
        $search_terms = $this->extractSearchTerms($query);
102
103
        // What trees to search?
104
        if (Site::getPreference('ALLOW_CHANGE_GEDCOM') === '1') {
105
            $all_trees = $this->tree_service->all();
106
        } else {
107
            $all_trees = new Collection([$tree]);
0 ignored issues
show
Bug introduced by
array($tree) of type array<integer,Fisharebest\Webtrees\Tree> is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

107
            $all_trees = new Collection(/** @scrutinizer ignore-type */ [$tree]);
Loading history...
108
        }
109
110
        $search_trees = $all_trees
111
            ->filter(static fn (Tree $tree): bool => in_array($tree->name(), $search_tree_names, true));
112
113
        if ($search_trees->isEmpty()) {
114
            $search_trees->add($tree);
115
        }
116
117
        // Do the search
118
        $individuals  = new Collection();
119
        $families     = new Collection();
120
        $locations    = new Collection();
121
        $repositories = new Collection();
122
        $sources      = new Collection();
123
        $notes        = new Collection();
124
125
        if ($search_terms !== []) {
126
            // Log search requests for visitors
127
            if (Auth::id() === null) {
128
                Log::addSearchLog('General: ' . $query, $search_trees->all());
129
            }
130
131
            if ($search_individuals) {
132
                $individuals = $this->search_service->searchIndividuals($search_trees->all(), $search_terms);
133
            }
134
135
            if ($search_families) {
136
                $tmp1 = $this->search_service->searchFamilies($search_trees->all(), $search_terms);
137
                $tmp2 = $this->search_service->searchFamilyNames($search_trees->all(), $search_terms);
138
139
                $families = $tmp1->merge($tmp2)->unique(static fn (Family $family): string => $family->xref() . '@' . $family->tree()->id());
140
            }
141
142
            if ($search_repositories) {
143
                $repositories = $this->search_service->searchRepositories($search_trees->all(), $search_terms);
144
            }
145
146
            if ($search_sources) {
147
                $sources = $this->search_service->searchSources($search_trees->all(), $search_terms);
148
            }
149
150
            if ($search_notes) {
151
                $notes = $this->search_service->searchNotes($search_trees->all(), $search_terms);
152
            }
153
154
            if ($search_locations) {
155
                $locations = $this->search_service->searchLocations($search_trees->all(), $search_terms);
156
            }
157
        }
158
159
        // If only 1 item is returned, automatically forward to that item
160
        if ($individuals->count() === 1 && $families->isEmpty() && $sources->isEmpty() && $notes->isEmpty() && $locations->isEmpty()) {
161
            return redirect($individuals->first()->url());
162
        }
163
164
        if ($individuals->isEmpty() && $families->count() === 1 && $sources->isEmpty() && $notes->isEmpty() && $locations->isEmpty()) {
165
            return redirect($families->first()->url());
166
        }
167
168
        if ($individuals->isEmpty() && $families->isEmpty() && $sources->count() === 1 && $notes->isEmpty() && $locations->isEmpty()) {
169
            return redirect($sources->first()->url());
170
        }
171
172
        if ($individuals->isEmpty() && $families->isEmpty() && $sources->isEmpty() && $notes->count() === 1 && $locations->isEmpty()) {
173
            return redirect($notes->first()->url());
174
        }
175
176
        if ($individuals->isEmpty() && $families->isEmpty() && $sources->isEmpty() && $notes->isEmpty() && $locations->count() === 1) {
177
            return redirect($locations->first()->url());
178
        }
179
180
        $title = I18N::translate('General search');
181
182
        return $this->viewResponse('search-general-page', [
183
            'all_trees'           => $all_trees,
184
            'exist_locations'     => $exist_locations,
185
            'exist_notes'         => $exist_notes,
186
            'exist_repositories'  => $exist_repositories,
187
            'exist_sources'       => $exist_sources,
188
            'families'            => $families,
189
            'individuals'         => $individuals,
190
            'locations'           => $locations,
191
            'notes'               => $notes,
192
            'query'               => $query,
193
            'repositories'        => $repositories,
194
            'search_families'     => $search_families,
195
            'search_individuals'  => $search_individuals,
196
            'search_locations'    => $search_locations,
197
            'search_notes'        => $search_notes,
198
            'search_repositories' => $search_repositories,
199
            'search_sources'      => $search_sources,
200
            'search_trees'        => $search_trees,
201
            'sources'             => $sources,
202
            'title'               => $title,
203
            'tree'                => $tree,
204
        ]);
205
    }
206
207
    /**
208
     * Convert the query into an array of search terms
209
     *
210
     * @param string $query
211
     *
212
     * @return array<string>
213
     */
214
    private function extractSearchTerms(string $query): array
215
    {
216
        $search_terms = [];
217
218
        // Words in double quotes stay together
219
        preg_match_all('/"([^"]+)"/', $query, $matches, PREG_SET_ORDER);
220
        foreach ($matches as $match) {
221
            $search_terms[] = trim($match[1]);
222
            // Remove this string from the search query
223
            $query = strtr($query, [$match[0] => '']);
224
        }
225
226
        // Treat CJK characters as separate words, not as characters.
227
        $query = preg_replace('/\p{Han}/u', '$0 ', $query);
228
229
        // Other words get treated separately
230
        preg_match_all('/[\S]+/', $query, $matches, PREG_SET_ORDER);
231
        foreach ($matches as $match) {
232
            $search_terms[] = $match[0];
233
        }
234
235
        return $search_terms;
236
    }
237
}
238