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