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