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