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