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 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 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