1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2022 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 Aura\Router\Route; 23use Fisharebest\Webtrees\Auth; 24use Fisharebest\Webtrees\Contracts\UserInterface; 25use Fisharebest\Webtrees\Fact; 26use Fisharebest\Webtrees\Gedcom; 27use Fisharebest\Webtrees\Http\RequestHandlers\AccountEdit; 28use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel; 29use Fisharebest\Webtrees\Http\RequestHandlers\HomePage; 30use Fisharebest\Webtrees\Http\RequestHandlers\LoginPage; 31use Fisharebest\Webtrees\Http\RequestHandlers\Logout; 32use Fisharebest\Webtrees\Http\RequestHandlers\ManageTrees; 33use Fisharebest\Webtrees\Http\RequestHandlers\PendingChanges; 34use Fisharebest\Webtrees\Http\RequestHandlers\SelectLanguage; 35use Fisharebest\Webtrees\Http\RequestHandlers\SelectTheme; 36use Fisharebest\Webtrees\Http\RequestHandlers\TreePage; 37use Fisharebest\Webtrees\Http\RequestHandlers\TreePageEdit; 38use Fisharebest\Webtrees\Http\RequestHandlers\UserPage; 39use Fisharebest\Webtrees\Http\RequestHandlers\UserPageEdit; 40use Fisharebest\Webtrees\I18N; 41use Fisharebest\Webtrees\Individual; 42use Fisharebest\Webtrees\Menu; 43use Fisharebest\Webtrees\Registry; 44use Fisharebest\Webtrees\Services\ModuleService; 45use Fisharebest\Webtrees\Tree; 46use Fisharebest\Webtrees\Validator; 47use PhpParser\Node\Expr\AssignOp\Mod; 48use Psr\Http\Message\ServerRequestInterface; 49 50use function app; 51use function assert; 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 = $request->getQueryParams()['url'] ?? (string) $request->getUri(); 286 287 $tree = Validator::attributes($request)->treeOptional(); 288 $route = Validator::attributes($request)->route(); 289 290 // ...but switch from the tree-page to the user-page 291 if ($route->name === TreePage::class) { 292 $redirect = route(UserPage::class, ['tree' => $tree instanceof Tree ? $tree->name() : null]); 293 } 294 295 // Stay on the same tree page 296 $url = route(LoginPage::class, ['tree' => $tree instanceof Tree ? $tree->name() : null, 'url' => $redirect]); 297 298 return new Menu(I18N::translate('Sign in'), $url, 'menu-login', ['rel' => 'nofollow']); 299 } 300 301 /** 302 * A logout menu option (or null if we are already logged out). 303 * 304 * @return Menu|null 305 */ 306 public function menuLogout(): ?Menu 307 { 308 if (Auth::check()) { 309 $parameters = [ 310 'data-wt-post-url' => route(Logout::class), 311 'data-wt-reload-url' => route(HomePage::class) 312 ]; 313 314 return new Menu(I18N::translate('Sign out'), '#', 'menu-logout', $parameters); 315 } 316 317 return null; 318 } 319 320 /** 321 * A link to allow users to edit their account settings. 322 * 323 * @param Tree|null $tree 324 * 325 * @return Menu 326 */ 327 public function menuMyAccount(?Tree $tree): Menu 328 { 329 $url = route(AccountEdit::class, ['tree' => $tree instanceof Tree ? $tree->name() : null]); 330 331 return new Menu(I18N::translate('My account'), $url, 'menu-myaccount'); 332 } 333 334 /** 335 * A link to the user's individual record (individual.php). 336 * 337 * @param Tree $tree 338 * 339 * @return Menu|null 340 */ 341 public function menuMyIndividualRecord(Tree $tree): ?Menu 342 { 343 $record = Registry::individualFactory()->make($tree->getUserPreference(Auth::user(), UserInterface::PREF_TREE_ACCOUNT_XREF), $tree); 344 345 if ($record) { 346 return new Menu(I18N::translate('My individual record'), $record->url(), 'menu-myrecord'); 347 } 348 349 return null; 350 } 351 352 /** 353 * A link to the user's personal home page. 354 * 355 * @param Tree $tree 356 * 357 * @return Menu 358 */ 359 public function menuMyPage(Tree $tree): Menu 360 { 361 return new Menu(I18N::translate('My page'), route(UserPage::class, ['tree' => $tree->name()]), 'menu-mypage'); 362 } 363 364 /** 365 * A menu for the user's personal pages. 366 * 367 * @param Tree|null $tree 368 * 369 * @return Menu|null 370 */ 371 public function menuMyPages(?Tree $tree): ?Menu 372 { 373 if (Auth::check()) { 374 if ($tree instanceof Tree) { 375 return new Menu(I18N::translate('My pages'), '#', 'menu-mymenu', [], array_filter([ 376 $this->menuMyPage($tree), 377 $this->menuMyIndividualRecord($tree), 378 $this->menuMyPedigree($tree), 379 $this->menuMyAccount($tree), 380 $this->menuControlPanel($tree), 381 $this->menuChangeBlocks($tree), 382 ])); 383 } 384 385 return $this->menuMyAccount($tree); 386 } 387 388 return null; 389 } 390 391 /** 392 * A link to the user's individual record. 393 * 394 * @param Tree $tree 395 * 396 * @return Menu|null 397 */ 398 public function menuMyPedigree(Tree $tree): ?Menu 399 { 400 $my_xref = $tree->getUserPreference(Auth::user(), UserInterface::PREF_TREE_ACCOUNT_XREF); 401 402 $module_service = app(ModuleService::class); 403 assert($module_service instanceof ModuleService); 404 405 $pedigree_chart = $module_service 406 ->findByComponent(ModuleChartInterface::class, $tree, Auth::user()) 407 ->first(static fn (ModuleInterface $module): bool => $module instanceof PedigreeChartModule); 408 409 if ($my_xref !== '' && $pedigree_chart instanceof PedigreeChartModule) { 410 $individual = Registry::individualFactory()->make($my_xref, $tree); 411 412 if ($individual instanceof Individual) { 413 return new Menu( 414 I18N::translate('My pedigree'), 415 $pedigree_chart->chartUrl($individual), 416 'menu-mypedigree' 417 ); 418 } 419 } 420 421 return null; 422 } 423 424 /** 425 * Create a pending changes menu. 426 * 427 * @param Tree|null $tree 428 * 429 * @return Menu|null 430 */ 431 public function menuPendingChanges(?Tree $tree): ?Menu 432 { 433 if ($tree instanceof Tree && $tree->hasPendingEdit() && Auth::isModerator($tree)) { 434 $request = app(ServerRequestInterface::class); 435 assert($request instanceof ServerRequestInterface); 436 437 $url = route(PendingChanges::class, [ 438 'tree' => $tree->name(), 439 'url' => (string) $request->getUri(), 440 ]); 441 442 return new Menu(I18N::translate('Pending changes'), $url, 'menu-pending'); 443 } 444 445 return null; 446 } 447 448 /** 449 * Themes menu. 450 * 451 * @return Menu|null 452 */ 453 public function menuThemes(): ?Menu 454 { 455 $module_service = app(ModuleService::class); 456 assert($module_service instanceof ModuleService); 457 458 $themes = $module_service->findByInterface(ModuleThemeInterface::class, false, true); 459 460 $current_theme = app(ModuleThemeInterface::class); 461 462 if ($themes->count() > 1) { 463 $submenus = $themes->map(static function (ModuleThemeInterface $theme) use ($current_theme): Menu { 464 $active = $theme->name() === $current_theme->name(); 465 $class = 'menu-theme-' . $theme->name() . ($active ? ' active' : ''); 466 467 return new Menu($theme->title(), '#', $class, [ 468 'data-wt-post-url' => route(SelectTheme::class, ['theme' => $theme->name()]), 469 ]); 470 }); 471 472 return new Menu(I18N::translate('Theme'), '#', 'menu-theme', [], $submenus->all()); 473 } 474 475 return null; 476 } 477 478 /** 479 * Generate a list of items for the main menu. 480 * 481 * @param Tree|null $tree 482 * 483 * @return array<Menu> 484 */ 485 public function genealogyMenu(?Tree $tree): array 486 { 487 if ($tree === null) { 488 return []; 489 } 490 491 $module_service = app(ModuleService::class); 492 assert($module_service instanceof ModuleService); 493 494 return $module_service 495 ->findByComponent(ModuleMenuInterface::class, $tree, Auth::user()) 496 ->map(static fn (ModuleMenuInterface $menu): ?Menu => $menu->getMenu($tree)) 497 ->filter() 498 ->all(); 499 } 500 501 /** 502 * Create the genealogy menu. 503 * 504 * @param array<Menu> $menus 505 * 506 * @return string 507 */ 508 public function genealogyMenuContent(array $menus): string 509 { 510 return implode('', array_map(static function (Menu $menu): string { 511 return view('components/menu-item', ['menu' => $menu]); 512 }, $menus)); 513 } 514 515 /** 516 * Generate a list of items for the user menu. 517 * 518 * @param Tree|null $tree 519 * 520 * @return array<Menu> 521 */ 522 public function userMenu(?Tree $tree): array 523 { 524 return array_filter([ 525 $this->menuPendingChanges($tree), 526 $this->menuMyPages($tree), 527 $this->menuThemes(), 528 $this->menuLanguages(), 529 $this->menuLogin(), 530 $this->menuLogout(), 531 ]); 532 } 533 534 /** 535 * A list of CSS files to include for this page. 536 * 537 * @return array<string> 538 */ 539 public function stylesheets(): array 540 { 541 return []; 542 } 543} 544