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 count; 49use function in_array; 50use function route; 51use function view; 52 53/** 54 * Trait ModuleThemeTrait - default implementation of ModuleThemeInterface 55 */ 56trait ModuleThemeTrait 57{ 58 /** 59 * How should this module be identified in the control panel, etc.? 60 * 61 * @return string 62 */ 63 abstract public function title(): 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 = Registry::container()->get(ModuleService::class); 151 152 foreach ($module_service->findByComponent(ModuleChartInterface::class, $individual->tree(), Auth::user()) as $chart) { 153 $menu = $chart->chartBoxMenu($individual); 154 if ($menu) { 155 $menus[] = $menu; 156 } 157 } 158 159 usort($menus, static fn (Menu $x, Menu $y): int => I18N::comparator()($x->getLabel(), $y->getLabel())); 160 161 return $menus; 162 } 163 164 /** 165 * Family links, to show in chart boxes. 166 * 167 * @param Individual $individual 168 * 169 * @return array<Menu> 170 */ 171 public function individualBoxMenuFamilyLinks(Individual $individual): array 172 { 173 $menus = []; 174 175 foreach ($individual->spouseFamilies() as $family) { 176 $menus[] = new Menu('<strong>' . I18N::translate('Family with spouse') . '</strong>', $family->url()); 177 $spouse = $family->spouse($individual); 178 if ($spouse && $spouse->canShowName()) { 179 $menus[] = new Menu($spouse->fullName(), $spouse->url()); 180 } 181 foreach ($family->children() as $child) { 182 if ($child->canShowName()) { 183 $menus[] = new Menu($child->fullName(), $child->url()); 184 } 185 } 186 } 187 188 return $menus; 189 } 190 191 /** 192 * Generate a menu item to change the blocks on the current tree/user page. 193 * 194 * @param Tree $tree 195 * 196 * @return Menu|null 197 */ 198 public function menuChangeBlocks(Tree $tree): Menu|null 199 { 200 $request = Registry::container()->get(ServerRequestInterface::class); 201 $route = Validator::attributes($request)->route(); 202 203 if (Auth::check() && $route->name === UserPage::class) { 204 return new Menu(I18N::translate('Customize this page'), route(UserPageEdit::class, ['tree' => $tree->name()]), 'menu-change-blocks'); 205 } 206 207 if (Auth::isManager($tree) && $route->name === TreePage::class) { 208 return new Menu(I18N::translate('Customize this page'), route(TreePageEdit::class, ['tree' => $tree->name()]), 'menu-change-blocks'); 209 } 210 211 return null; 212 } 213 214 /** 215 * Generate a menu item for the control panel. 216 * 217 * @param Tree $tree 218 * 219 * @return Menu|null 220 */ 221 public function menuControlPanel(Tree $tree): Menu|null 222 { 223 if (Auth::isAdmin()) { 224 return new Menu(I18N::translate('Control panel'), route(ControlPanel::class), 'menu-admin'); 225 } 226 227 if (Auth::isManager($tree)) { 228 return new Menu(I18N::translate('Control panel'), route(ManageTrees::class, ['tree' => $tree->name()]), 'menu-admin'); 229 } 230 231 return null; 232 } 233 234 /** 235 * A menu to show a list of available languages. 236 * 237 * @return Menu|null 238 */ 239 public function menuLanguages(): Menu|null 240 { 241 $menu = new Menu(I18N::translate('Language'), '#', 'menu-language'); 242 243 foreach (I18N::activeLocales() as $active_locale) { 244 $language_tag = $active_locale->languageTag(); 245 $class = 'menu-language-' . $language_tag . (I18N::languageTag() === $language_tag ? ' active' : ''); 246 $menu->addSubmenu(new Menu($active_locale->endonym(), '#', $class, [ 247 'data-wt-post-url' => route(SelectLanguage::class, ['language' => $language_tag]), 248 ])); 249 } 250 251 if (count($menu->getSubmenus()) > 1) { 252 return $menu; 253 } 254 255 return null; 256 } 257 258 /** 259 * A login menu option (or null if we are already logged in). 260 * 261 * @return Menu|null 262 */ 263 public function menuLogin(): Menu|null 264 { 265 if (Auth::check()) { 266 return null; 267 } 268 269 $request = Registry::container()->get(ServerRequestInterface::class); 270 271 // Return to this page after login... 272 $redirect = Validator::queryParams($request)->string('url', (string) $request->getUri()); 273 $tree = Validator::attributes($request)->treeOptional(); 274 $route = Validator::attributes($request)->route(); 275 276 // ...but switch from the tree-page to the user-page 277 if ($route->name === TreePage::class) { 278 $redirect = route(UserPage::class, ['tree' => $tree?->name()]); 279 } 280 281 // Stay on the same tree page 282 $url = route(LoginPage::class, ['tree' => $tree?->name(), 'url' => $redirect]); 283 284 return new Menu(I18N::translate('Sign in'), $url, 'menu-login', ['rel' => 'nofollow']); 285 } 286 287 /** 288 * A logout menu option (or null if we are already logged out). 289 * 290 * @return Menu|null 291 */ 292 public function menuLogout(): Menu|null 293 { 294 if (Auth::check()) { 295 $parameters = [ 296 'data-wt-post-url' => route(Logout::class), 297 'data-wt-reload-url' => route(HomePage::class) 298 ]; 299 300 return new Menu(I18N::translate('Sign out'), '#', 'menu-logout', $parameters); 301 } 302 303 return null; 304 } 305 306 /** 307 * A link to allow users to edit their account settings. 308 * 309 * @param Tree|null $tree 310 * 311 * @return Menu 312 */ 313 public function menuMyAccount(Tree|null $tree): Menu 314 { 315 $url = route(AccountEdit::class, ['tree' => $tree?->name()]); 316 317 return new Menu(I18N::translate('My account'), $url, 'menu-myaccount'); 318 } 319 320 /** 321 * A link to the user's individual record (individual.php). 322 * 323 * @param Tree $tree 324 * 325 * @return Menu|null 326 */ 327 public function menuMyIndividualRecord(Tree $tree): Menu|null 328 { 329 $record = Registry::individualFactory()->make($tree->getUserPreference(Auth::user(), UserInterface::PREF_TREE_ACCOUNT_XREF), $tree); 330 331 if ($record instanceof Individual) { 332 return new Menu(I18N::translate('My individual record'), $record->url(), 'menu-myrecord'); 333 } 334 335 return null; 336 } 337 338 /** 339 * A link to the user's personal home page. 340 * 341 * @param Tree $tree 342 * 343 * @return Menu 344 */ 345 public function menuMyPage(Tree $tree): Menu 346 { 347 return new Menu(I18N::translate('My page'), route(UserPage::class, ['tree' => $tree->name()]), 'menu-mypage'); 348 } 349 350 /** 351 * A menu for the user's personal pages. 352 * 353 * @param Tree|null $tree 354 * 355 * @return Menu|null 356 */ 357 public function menuMyPages(Tree|null $tree): Menu|null 358 { 359 if (Auth::check()) { 360 if ($tree instanceof Tree) { 361 return new Menu(I18N::translate('My pages'), '#', 'menu-mymenu', [], array_filter([ 362 $this->menuMyPage($tree), 363 $this->menuMyIndividualRecord($tree), 364 $this->menuMyPedigree($tree), 365 $this->menuMyAccount($tree), 366 $this->menuControlPanel($tree), 367 $this->menuChangeBlocks($tree), 368 ])); 369 } 370 371 return $this->menuMyAccount($tree); 372 } 373 374 return null; 375 } 376 377 /** 378 * A link to the user's individual record. 379 * 380 * @param Tree $tree 381 * 382 * @return Menu|null 383 */ 384 public function menuMyPedigree(Tree $tree): Menu|null 385 { 386 $my_xref = $tree->getUserPreference(Auth::user(), UserInterface::PREF_TREE_ACCOUNT_XREF); 387 388 $module_service = Registry::container()->get(ModuleService::class); 389 $pedigree_chart = $module_service 390 ->findByComponent(ModuleChartInterface::class, $tree, Auth::user()) 391 ->first(static fn (ModuleInterface $module): bool => $module instanceof PedigreeChartModule); 392 393 if ($my_xref !== '' && $pedigree_chart instanceof PedigreeChartModule) { 394 $individual = Registry::individualFactory()->make($my_xref, $tree); 395 396 if ($individual instanceof Individual) { 397 return new Menu( 398 I18N::translate('My pedigree'), 399 $pedigree_chart->chartUrl($individual), 400 'menu-mypedigree' 401 ); 402 } 403 } 404 405 return null; 406 } 407 408 /** 409 * Create a pending changes menu. 410 * 411 * @param Tree|null $tree 412 * 413 * @return Menu|null 414 */ 415 public function menuPendingChanges(Tree|null $tree): Menu|null 416 { 417 if ($tree instanceof Tree && $tree->hasPendingEdit() && Auth::isModerator($tree)) { 418 $request = Registry::container()->get(ServerRequestInterface::class); 419 420 $url = route(PendingChanges::class, [ 421 'tree' => $tree->name(), 422 'url' => (string) $request->getUri(), 423 ]); 424 425 return new Menu(I18N::translate('Pending changes'), $url, 'menu-pending'); 426 } 427 428 return null; 429 } 430 431 /** 432 * Themes menu. 433 * 434 * @return Menu|null 435 */ 436 public function menuThemes(): Menu|null 437 { 438 $module_service = Registry::container()->get(ModuleService::class); 439 $themes = $module_service->findByInterface(ModuleThemeInterface::class, false, true); 440 $current_theme = Registry::container()->get(ModuleThemeInterface::class); 441 442 if ($themes->count() > 1) { 443 $submenus = $themes->map(static function (ModuleThemeInterface $theme) use ($current_theme): Menu { 444 $active = $theme->name() === $current_theme->name(); 445 $class = 'menu-theme-' . $theme->name() . ($active ? ' active' : ''); 446 447 return new Menu($theme->title(), '#', $class, [ 448 'data-wt-post-url' => route(SelectTheme::class, ['theme' => $theme->name()]), 449 ]); 450 }); 451 452 return new Menu(I18N::translate('Theme'), '#', 'menu-theme', [], $submenus->all()); 453 } 454 455 return null; 456 } 457 458 /** 459 * Generate a list of items for the main menu. 460 * 461 * @param Tree|null $tree 462 * 463 * @return array<Menu> 464 */ 465 public function genealogyMenu(Tree|null $tree): array 466 { 467 if ($tree === null) { 468 return []; 469 } 470 471 $module_service = Registry::container()->get(ModuleService::class); 472 473 return $module_service 474 ->findByComponent(ModuleMenuInterface::class, $tree, Auth::user()) 475 ->map(static fn (ModuleMenuInterface $menu): Menu|null => $menu->getMenu($tree)) 476 ->filter() 477 ->all(); 478 } 479 480 /** 481 * Create the genealogy menu. 482 * 483 * @param array<Menu> $menus 484 * 485 * @return string 486 */ 487 public function genealogyMenuContent(array $menus): string 488 { 489 return implode('', array_map(static fn (Menu $menu): string => view('components/menu-item', ['menu' => $menu]), $menus)); 490 } 491 492 /** 493 * Generate a list of items for the user menu. 494 * 495 * @param Tree|null $tree 496 * 497 * @return array<Menu> 498 */ 499 public function userMenu(Tree|null $tree): array 500 { 501 return array_filter([ 502 $this->menuPendingChanges($tree), 503 $this->menuMyPages($tree), 504 $this->menuThemes(), 505 $this->menuLanguages(), 506 $this->menuLogin(), 507 $this->menuLogout(), 508 ]); 509 } 510 511 /** 512 * A list of CSS files to include for this page. 513 * 514 * @return array<string> 515 */ 516 public function stylesheets(): array 517 { 518 return []; 519 } 520} 521