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