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\Module; 21 22use Fisharebest\Webtrees\Auth; 23use Fisharebest\Webtrees\Contracts\UserInterface; 24use Fisharebest\Webtrees\Fact; 25use Fisharebest\Webtrees\Gedcom; 26use Fisharebest\Webtrees\Http\RequestHandlers\AccountEdit; 27use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel; 28use Fisharebest\Webtrees\Http\RequestHandlers\HomePage; 29use Fisharebest\Webtrees\Http\RequestHandlers\LoginPage; 30use Fisharebest\Webtrees\Http\RequestHandlers\Logout; 31use Fisharebest\Webtrees\Http\RequestHandlers\ManageTrees; 32use Fisharebest\Webtrees\Http\RequestHandlers\PendingChanges; 33use Fisharebest\Webtrees\Http\RequestHandlers\SelectLanguage; 34use Fisharebest\Webtrees\Http\RequestHandlers\SelectTheme; 35use Fisharebest\Webtrees\Http\RequestHandlers\TreePage; 36use Fisharebest\Webtrees\Http\RequestHandlers\TreePageEdit; 37use Fisharebest\Webtrees\Http\RequestHandlers\UserPage; 38use Fisharebest\Webtrees\Http\RequestHandlers\UserPageEdit; 39use Fisharebest\Webtrees\I18N; 40use Fisharebest\Webtrees\Individual; 41use Fisharebest\Webtrees\Menu; 42use Fisharebest\Webtrees\Registry; 43use Fisharebest\Webtrees\Services\ModuleService; 44use Fisharebest\Webtrees\Tree; 45use Fisharebest\Webtrees\Validator; 46use Psr\Http\Message\ServerRequestInterface; 47 48use function app; 49use function assert; 50use function count; 51use function in_array; 52use function route; 53use function view; 54 55/** 56 * Trait ModuleThemeTrait - default implementation of ModuleThemeInterface 57 */ 58trait ModuleThemeTrait 59{ 60 /** 61 * How should this module be identified in the control panel, etc.? 62 * 63 * @return string 64 */ 65 abstract public function title(): string; 66 67 /** 68 * A sentence describing what this module does. 69 * 70 * @return string 71 */ 72 public function description(): string 73 { 74 return I18N::translate('Theme') . ' — ' . $this->title(); 75 } 76 77 /** 78 * Generate the facts, for display in charts. 79 * 80 * @param Individual $individual 81 * 82 * @return string 83 */ 84 public function individualBoxFacts(Individual $individual): string 85 { 86 $html = ''; 87 88 $opt_tags = preg_split('/\W/', $individual->tree()->getPreference('CHART_BOX_TAGS'), 0, PREG_SPLIT_NO_EMPTY); 89 // Show BIRT or equivalent event 90 foreach (Gedcom::BIRTH_EVENTS as $birttag) { 91 if (!in_array($birttag, $opt_tags, true)) { 92 $event = $individual->facts([$birttag])->first(); 93 if ($event instanceof Fact) { 94 $html .= $event->summary(); 95 break; 96 } 97 } 98 } 99 // Show optional events (before death) 100 foreach ($opt_tags as $key => $tag) { 101 if (!in_array($tag, Gedcom::DEATH_EVENTS, true)) { 102 $event = $individual->facts([$tag])->first(); 103 if ($event instanceof Fact) { 104 $html .= $event->summary(); 105 unset($opt_tags[$key]); 106 } 107 } 108 } 109 // Show DEAT or equivalent event 110 foreach (Gedcom::DEATH_EVENTS as $deattag) { 111 $event = $individual->facts([$deattag])->first(); 112 if ($event instanceof Fact) { 113 $html .= $event->summary(); 114 if (in_array($deattag, $opt_tags, true)) { 115 unset($opt_tags[array_search($deattag, $opt_tags, true)]); 116 } 117 break; 118 } 119 } 120 // Show remaining optional events (after death) 121 foreach ($opt_tags as $tag) { 122 $event = $individual->facts([$tag])->first(); 123 if ($event instanceof Fact) { 124 $html .= $event->summary(); 125 } 126 } 127 128 return $html; 129 } 130 131 /** 132 * Links, to show in chart boxes; 133 * 134 * @param Individual $individual 135 * 136 * @return array<Menu> 137 */ 138 public function individualBoxMenu(Individual $individual): array 139 { 140 return array_merge( 141 $this->individualBoxMenuCharts($individual), 142 $this->individualBoxMenuFamilyLinks($individual) 143 ); 144 } 145 146 /** 147 * Chart links, to show in chart boxes; 148 * 149 * @param Individual $individual 150 * 151 * @return array<Menu> 152 */ 153 public function individualBoxMenuCharts(Individual $individual): array 154 { 155 $menus = []; 156 157 $module_service = app(ModuleService::class); 158 assert($module_service instanceof ModuleService); 159 160 foreach ($module_service->findByComponent(ModuleChartInterface::class, $individual->tree(), Auth::user()) as $chart) { 161 $menu = $chart->chartBoxMenu($individual); 162 if ($menu) { 163 $menus[] = $menu; 164 } 165 } 166 167 usort($menus, static function (Menu $x, Menu $y): int { 168 return I18N::comparator()($x->getLabel(), $y->getLabel()); 169 }); 170 171 return $menus; 172 } 173 174 /** 175 * Family links, to show in chart boxes. 176 * 177 * @param Individual $individual 178 * 179 * @return array<Menu> 180 */ 181 public function individualBoxMenuFamilyLinks(Individual $individual): array 182 { 183 $menus = []; 184 185 foreach ($individual->spouseFamilies() as $family) { 186 $menus[] = new Menu('<strong>' . I18N::translate('Family with spouse') . '</strong>', $family->url()); 187 $spouse = $family->spouse($individual); 188 if ($spouse && $spouse->canShowName()) { 189 $menus[] = new Menu($spouse->fullName(), $spouse->url()); 190 } 191 foreach ($family->children() as $child) { 192 if ($child->canShowName()) { 193 $menus[] = new Menu($child->fullName(), $child->url()); 194 } 195 } 196 } 197 198 return $menus; 199 } 200 201 /** 202 * Generate a menu item to change the blocks on the current tree/user page. 203 * 204 * @param Tree $tree 205 * 206 * @return Menu|null 207 */ 208 public function menuChangeBlocks(Tree $tree): ?Menu 209 { 210 $request = app(ServerRequestInterface::class); 211 assert($request instanceof ServerRequestInterface); 212 213 $route = Validator::attributes($request)->route(); 214 215 if (Auth::check() && $route->name === UserPage::class) { 216 return new Menu(I18N::translate('Customize this page'), route(UserPageEdit::class, ['tree' => $tree->name()]), 'menu-change-blocks'); 217 } 218 219 if (Auth::isManager($tree) && $route->name === TreePage::class) { 220 return new Menu(I18N::translate('Customize this page'), route(TreePageEdit::class, ['tree' => $tree->name()]), 'menu-change-blocks'); 221 } 222 223 return null; 224 } 225 226 /** 227 * Generate a menu item for the control panel. 228 * 229 * @param Tree $tree 230 * 231 * @return Menu|null 232 */ 233 public function menuControlPanel(Tree $tree): ?Menu 234 { 235 if (Auth::isAdmin()) { 236 return new Menu(I18N::translate('Control panel'), route(ControlPanel::class), 'menu-admin'); 237 } 238 239 if (Auth::isManager($tree)) { 240 return new Menu(I18N::translate('Control panel'), route(ManageTrees::class, ['tree' => $tree->name()]), 'menu-admin'); 241 } 242 243 return null; 244 } 245 246 /** 247 * A menu to show a list of available languages. 248 * 249 * @return Menu|null 250 */ 251 public function menuLanguages(): ?Menu 252 { 253 $menu = new Menu(I18N::translate('Language'), '#', 'menu-language'); 254 255 foreach (I18N::activeLocales() as $active_locale) { 256 $language_tag = $active_locale->languageTag(); 257 $class = 'menu-language-' . $language_tag . (I18N::languageTag() === $language_tag ? ' active' : ''); 258 $menu->addSubmenu(new Menu($active_locale->endonym(), '#', $class, [ 259 'data-wt-post-url' => route(SelectLanguage::class, ['language' => $language_tag]), 260 ])); 261 } 262 263 if (count($menu->getSubmenus()) > 1) { 264 return $menu; 265 } 266 267 return null; 268 } 269 270 /** 271 * A login menu option (or null if we are already logged in). 272 * 273 * @return Menu|null 274 */ 275 public function menuLogin(): ?Menu 276 { 277 if (Auth::check()) { 278 return null; 279 } 280 281 $request = app(ServerRequestInterface::class); 282 assert($request instanceof ServerRequestInterface); 283 284 // Return to this page after login... 285 $redirect = Validator::queryParams($request)->string('url', (string) $request->getUri()); 286 $tree = Validator::attributes($request)->treeOptional(); 287 $route = Validator::attributes($request)->route(); 288 289 // ...but switch from the tree-page to the user-page 290 if ($route->name === TreePage::class) { 291 $redirect = route(UserPage::class, ['tree' => $tree?->name()]); 292 } 293 294 // Stay on the same tree page 295 $url = route(LoginPage::class, ['tree' => $tree?->name(), 'url' => $redirect]); 296 297 return new Menu(I18N::translate('Sign in'), $url, 'menu-login', ['rel' => 'nofollow']); 298 } 299 300 /** 301 * A logout menu option (or null if we are already logged out). 302 * 303 * @return Menu|null 304 */ 305 public function menuLogout(): ?Menu 306 { 307 if (Auth::check()) { 308 $parameters = [ 309 'data-wt-post-url' => route(Logout::class), 310 'data-wt-reload-url' => route(HomePage::class) 311 ]; 312 313 return new Menu(I18N::translate('Sign out'), '#', 'menu-logout', $parameters); 314 } 315 316 return null; 317 } 318 319 /** 320 * A link to allow users to edit their account settings. 321 * 322 * @param Tree|null $tree 323 * 324 * @return Menu 325 */ 326 public function menuMyAccount(?Tree $tree): Menu 327 { 328 $url = route(AccountEdit::class, ['tree' => $tree?->name()]); 329 330 return new Menu(I18N::translate('My account'), $url, 'menu-myaccount'); 331 } 332 333 /** 334 * A link to the user's individual record (individual.php). 335 * 336 * @param Tree $tree 337 * 338 * @return Menu|null 339 */ 340 public function menuMyIndividualRecord(Tree $tree): ?Menu 341 { 342 $record = Registry::individualFactory()->make($tree->getUserPreference(Auth::user(), UserInterface::PREF_TREE_ACCOUNT_XREF), $tree); 343 344 if ($record) { 345 return new Menu(I18N::translate('My individual record'), $record->url(), 'menu-myrecord'); 346 } 347 348 return null; 349 } 350 351 /** 352 * A link to the user's personal home page. 353 * 354 * @param Tree $tree 355 * 356 * @return Menu 357 */ 358 public function menuMyPage(Tree $tree): Menu 359 { 360 return new Menu(I18N::translate('My page'), route(UserPage::class, ['tree' => $tree->name()]), 'menu-mypage'); 361 } 362 363 /** 364 * A menu for the user's personal pages. 365 * 366 * @param Tree|null $tree 367 * 368 * @return Menu|null 369 */ 370 public function menuMyPages(?Tree $tree): ?Menu 371 { 372 if (Auth::check()) { 373 if ($tree instanceof Tree) { 374 return new Menu(I18N::translate('My pages'), '#', 'menu-mymenu', [], array_filter([ 375 $this->menuMyPage($tree), 376 $this->menuMyIndividualRecord($tree), 377 $this->menuMyPedigree($tree), 378 $this->menuMyAccount($tree), 379 $this->menuControlPanel($tree), 380 $this->menuChangeBlocks($tree), 381 ])); 382 } 383 384 return $this->menuMyAccount($tree); 385 } 386 387 return null; 388 } 389 390 /** 391 * A link to the user's individual record. 392 * 393 * @param Tree $tree 394 * 395 * @return Menu|null 396 */ 397 public function menuMyPedigree(Tree $tree): ?Menu 398 { 399 $my_xref = $tree->getUserPreference(Auth::user(), UserInterface::PREF_TREE_ACCOUNT_XREF); 400 401 $module_service = app(ModuleService::class); 402 assert($module_service instanceof ModuleService); 403 404 $pedigree_chart = $module_service 405 ->findByComponent(ModuleChartInterface::class, $tree, Auth::user()) 406 ->first(static fn (ModuleInterface $module): bool => $module instanceof PedigreeChartModule); 407 408 if ($my_xref !== '' && $pedigree_chart instanceof PedigreeChartModule) { 409 $individual = Registry::individualFactory()->make($my_xref, $tree); 410 411 if ($individual instanceof Individual) { 412 return new Menu( 413 I18N::translate('My pedigree'), 414 $pedigree_chart->chartUrl($individual), 415 'menu-mypedigree' 416 ); 417 } 418 } 419 420 return null; 421 } 422 423 /** 424 * Create a pending changes menu. 425 * 426 * @param Tree|null $tree 427 * 428 * @return Menu|null 429 */ 430 public function menuPendingChanges(?Tree $tree): ?Menu 431 { 432 if ($tree instanceof Tree && $tree->hasPendingEdit() && Auth::isModerator($tree)) { 433 $request = app(ServerRequestInterface::class); 434 assert($request instanceof ServerRequestInterface); 435 436 $url = route(PendingChanges::class, [ 437 'tree' => $tree->name(), 438 'url' => (string) $request->getUri(), 439 ]); 440 441 return new Menu(I18N::translate('Pending changes'), $url, 'menu-pending'); 442 } 443 444 return null; 445 } 446 447 /** 448 * Themes menu. 449 * 450 * @return Menu|null 451 */ 452 public function menuThemes(): ?Menu 453 { 454 $module_service = app(ModuleService::class); 455 assert($module_service instanceof ModuleService); 456 457 $themes = $module_service->findByInterface(ModuleThemeInterface::class, false, true); 458 459 $current_theme = app(ModuleThemeInterface::class); 460 461 if ($themes->count() > 1) { 462 $submenus = $themes->map(static function (ModuleThemeInterface $theme) use ($current_theme): Menu { 463 $active = $theme->name() === $current_theme->name(); 464 $class = 'menu-theme-' . $theme->name() . ($active ? ' active' : ''); 465 466 return new Menu($theme->title(), '#', $class, [ 467 'data-wt-post-url' => route(SelectTheme::class, ['theme' => $theme->name()]), 468 ]); 469 }); 470 471 return new Menu(I18N::translate('Theme'), '#', 'menu-theme', [], $submenus->all()); 472 } 473 474 return null; 475 } 476 477 /** 478 * Generate a list of items for the main menu. 479 * 480 * @param Tree|null $tree 481 * 482 * @return array<Menu> 483 */ 484 public function genealogyMenu(?Tree $tree): array 485 { 486 if ($tree === null) { 487 return []; 488 } 489 490 $module_service = app(ModuleService::class); 491 assert($module_service instanceof ModuleService); 492 493 return $module_service 494 ->findByComponent(ModuleMenuInterface::class, $tree, Auth::user()) 495 ->map(static fn (ModuleMenuInterface $menu): ?Menu => $menu->getMenu($tree)) 496 ->filter() 497 ->all(); 498 } 499 500 /** 501 * Create the genealogy menu. 502 * 503 * @param array<Menu> $menus 504 * 505 * @return string 506 */ 507 public function genealogyMenuContent(array $menus): string 508 { 509 return implode('', array_map(static function (Menu $menu): string { 510 return view('components/menu-item', ['menu' => $menu]); 511 }, $menus)); 512 } 513 514 /** 515 * Generate a list of items for the user menu. 516 * 517 * @param Tree|null $tree 518 * 519 * @return array<Menu> 520 */ 521 public function userMenu(?Tree $tree): array 522 { 523 return array_filter([ 524 $this->menuPendingChanges($tree), 525 $this->menuMyPages($tree), 526 $this->menuThemes(), 527 $this->menuLanguages(), 528 $this->menuLogin(), 529 $this->menuLogout(), 530 ]); 531 } 532 533 /** 534 * A list of CSS files to include for this page. 535 * 536 * @return array<string> 537 */ 538 public function stylesheets(): array 539 { 540 return []; 541 } 542} 543