xref: /webtrees/app/Http/RequestHandlers/SearchAdvancedPage.php (revision e873f434551745f888937263ff89e80db3b0f785)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2023 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\Auth;
23use Fisharebest\Webtrees\Http\ViewResponseTrait;
24use Fisharebest\Webtrees\I18N;
25use Fisharebest\Webtrees\Log;
26use Fisharebest\Webtrees\Registry;
27use Fisharebest\Webtrees\Services\SearchService;
28use Fisharebest\Webtrees\Validator;
29use Illuminate\Support\Collection;
30use Psr\Http\Message\ResponseInterface;
31use Psr\Http\Message\ServerRequestInterface;
32use Psr\Http\Server\RequestHandlerInterface;
33
34use function array_fill_keys;
35use function array_filter;
36use function array_key_exists;
37use function array_keys;
38use function array_map;
39use function array_merge;
40use function implode;
41use function strtr;
42
43/**
44 * Search for genealogy data
45 */
46class SearchAdvancedPage implements RequestHandlerInterface
47{
48    use ViewResponseTrait;
49
50    private const array DEFAULT_ADVANCED_FIELDS = [
51        'INDI:NAME:GIVN',
52        'INDI:NAME:SURN',
53        'INDI:BIRT:DATE',
54        'INDI:BIRT:PLAC',
55        'FAM:MARR:DATE',
56        'FAM:MARR:PLAC',
57        'INDI:DEAT:DATE',
58        'INDI:DEAT:PLAC',
59        'FATHER:NAME:GIVN',
60        'FATHER:NAME:SURN',
61        'MOTHER:NAME:GIVN',
62        'MOTHER:NAME:SURN',
63    ];
64
65    private const array OTHER_ADVANCED_FIELDS = [
66        'INDI:ADOP:DATE',
67        'INDI:ADOP:PLAC',
68        'INDI:AFN',
69        'INDI:BAPL:DATE',
70        'INDI:BAPL:PLAC',
71        'INDI:BAPM:DATE',
72        'INDI:BAPM:PLAC',
73        'INDI:BARM:DATE',
74        'INDI:BARM:PLAC',
75        'INDI:BASM:DATE',
76        'INDI:BASM:PLAC',
77        'INDI:BLES:DATE',
78        'INDI:BLES:PLAC',
79        'INDI:BURI:DATE',
80        'INDI:BURI:PLAC',
81        'INDI:CENS:DATE',
82        'INDI:CENS:PLAC',
83        'INDI:CHAN:DATE',
84        'INDI:CHAN:_WT_USER',
85        'INDI:CHR:DATE',
86        'INDI:CHR:PLAC',
87        'INDI:CREM:DATE',
88        'INDI:CREM:PLAC',
89        'INDI:DSCR',
90        'INDI:EMIG:DATE',
91        'INDI:EMIG:PLAC',
92        'INDI:ENDL:DATE',
93        'INDI:ENDL:PLAC',
94        'INDI:EVEN',
95        'INDI:EVEN:TYPE',
96        'INDI:EVEN:DATE',
97        'INDI:EVEN:PLAC',
98        'INDI:FACT',
99        'INDI:FACT:TYPE',
100        'INDI:FCOM:DATE',
101        'INDI:FCOM:PLAC',
102        'INDI:IMMI:DATE',
103        'INDI:IMMI:PLAC',
104        'INDI:NAME:NICK',
105        'INDI:NAME:_MARNM',
106        'INDI:NAME:_HEB',
107        'INDI:NAME:ROMN',
108        'INDI:NATI',
109        'INDI:NATU:DATE',
110        'INDI:NATU:PLAC',
111        'INDI:NOTE',
112        'INDI:OCCU',
113        'INDI:ORDN:DATE',
114        'INDI:ORDN:PLAC',
115        'INDI:REFN',
116        'INDI:RELI',
117        'INDI:RESI:DATE',
118        'INDI:RESI:EMAIL',
119        'INDI:RESI:PLAC',
120        'INDI:SLGC:DATE',
121        'INDI:SLGC:PLAC',
122        'INDI:TITL',
123        'FAM:DIV:DATE',
124        'FAM:SLGS:DATE',
125        'FAM:SLGS:PLAC',
126    ];
127
128    private SearchService $search_service;
129
130    /**
131     * @param SearchService $search_service
132     */
133    public function __construct(SearchService $search_service)
134    {
135        $this->search_service = $search_service;
136    }
137
138    /**
139     * @param ServerRequestInterface $request
140     *
141     * @return ResponseInterface
142     */
143    public function handle(ServerRequestInterface $request): ResponseInterface
144    {
145        $tree           = Validator::attributes($request)->tree();
146        $default_fields = array_fill_keys(self::DEFAULT_ADVANCED_FIELDS, '');
147        $fields         = Validator::queryParams($request)->array('fields') ?: $default_fields;
148        $modifiers      = Validator::queryParams($request)->array('modifiers');
149        $other_fields   = $this->otherFields($fields);
150        $date_options   = $this->dateOptions();
151        $name_options   = $this->nameOptions();
152
153        $fields = array_map(static fn (string $x): string => preg_replace('/^\s+|\s+$/uD', '', $x), $fields);
154
155        $search_fields = array_filter($fields, static fn (string $x): bool => $x !== '');
156
157        if ($search_fields !== []) {
158            // Log search requests for visitors
159            if (Auth::id() === null) {
160                $fn      = static fn (string $x, string $y): string => $x . '=' . $y;
161                $message = 'Advanced: ' . implode(', ', array_map($fn, array_keys($search_fields), $search_fields));
162                Log::addSearchLog($message, [$tree]);
163            }
164
165            $individuals = $this->search_service->searchIndividualsAdvanced($tree, $search_fields, $modifiers);
166        } else {
167            $individuals = new Collection();
168        }
169
170        $title = I18N::translate('Advanced search');
171
172        return $this->viewResponse('search-advanced-page', [
173            'date_options' => $date_options,
174            'fields'       => $fields,
175            'field_labels' => $this->fieldLabels(),
176            'individuals'  => $individuals,
177            'modifiers'    => $modifiers,
178            'name_options' => $name_options,
179            'other_fields' => $other_fields,
180            'title'        => $title,
181            'tree'         => $tree,
182        ]);
183    }
184
185    /**
186     * Extra search fields to add to the advanced search
187     *
188     * @param array<string> $fields
189     *
190     * @return array<string,string>
191     */
192    private function otherFields(array $fields): array
193    {
194        $default_facts = new Collection(self::OTHER_ADVANCED_FIELDS);
195
196        $comparator = static function (string $x, string $y): int {
197            $element_factory = Registry::elementFactory();
198
199            $label1 = $element_factory->make(strtr($x, [':DATE' => '', ':PLAC' => '', ':TYPE' => '']))->label();
200            $label2 = $element_factory->make(strtr($y, [':DATE' => '', ':PLAC' => '', ':TYPE' => '']))->label();
201
202            return I18N::comparator()($label1, $label2) ?: strcmp($x, $y);
203        };
204
205        return $default_facts
206            ->reject(fn (string $field): bool => array_key_exists($field, $fields))
207            ->sort($comparator)
208            ->mapWithKeys(fn (string $fact): array => [$fact => Registry::elementFactory()->make($fact)->label()])
209            ->all();
210    }
211
212    /**
213     * We use some pseudo-GEDCOM tags for some of our fields.
214     *
215     * @return array<string,string>
216     */
217    private function fieldLabels(): array
218    {
219        $return = [];
220
221        foreach (array_merge(self::OTHER_ADVANCED_FIELDS, self::DEFAULT_ADVANCED_FIELDS) as $field) {
222            $tmp = strtr($field, ['MOTHER:' => 'INDI:', 'FATHER:' => 'INDI:']);
223            $return[$field] = Registry::elementFactory()->make($tmp)->label();
224        }
225
226        return $return;
227    }
228
229    /**
230     * For the advanced search
231     *
232     * @return array<string>
233     */
234    private function dateOptions(): array
235    {
236        return [
237            0  => I18N::translate('Exact date'),
238            1  => I18N::plural('±%s year', '±%s years', 1, I18N::number(1)),
239            2  => I18N::plural('±%s year', '±%s years', 2, I18N::number(2)),
240            5  => I18N::plural('±%s year', '±%s years', 5, I18N::number(5)),
241            10 => I18N::plural('±%s year', '±%s years', 10, I18N::number(10)),
242            20 => I18N::plural('±%s year', '±%s years', 20, I18N::number(20)),
243        ];
244    }
245
246    /**
247     * For the advanced search
248     *
249     * @return array<string>
250     */
251    private function nameOptions(): array
252    {
253        return [
254            'EXACT'    => I18N::translate('Exact'),
255            'BEGINS'   => I18N::translate('Begins with'),
256            'CONTAINS' => I18N::translate('Contains'),
257            'SDX'      => I18N::translate('Sounds like'),
258        ];
259    }
260}
261