1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2019 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 <http://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\Fact; 25use Fisharebest\Webtrees\Gedcom; 26use Fisharebest\Webtrees\Http\RequestHandlers\AccountEdit; 27use Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel; 28use Fisharebest\Webtrees\Http\RequestHandlers\LoginPage; 29use Fisharebest\Webtrees\Http\RequestHandlers\Logout; 30use Fisharebest\Webtrees\Http\RequestHandlers\PendingChanges; 31use Fisharebest\Webtrees\Http\RequestHandlers\SelectLanguage; 32use Fisharebest\Webtrees\Http\RequestHandlers\SelectTheme; 33use Fisharebest\Webtrees\Http\RequestHandlers\TreePage; 34use Fisharebest\Webtrees\Http\RequestHandlers\TreePageEdit; 35use Fisharebest\Webtrees\Http\RequestHandlers\UserPage; 36use Fisharebest\Webtrees\Http\RequestHandlers\UserPageEdit; 37use Fisharebest\Webtrees\I18N; 38use Fisharebest\Webtrees\Individual; 39use Fisharebest\Webtrees\Menu; 40use Fisharebest\Webtrees\Services\ModuleService; 41use Fisharebest\Webtrees\Tree; 42use Fisharebest\Webtrees\User; 43use Psr\Http\Message\ServerRequestInterface; 44 45use function app; 46use function assert; 47use function route; 48 49/** 50 * Trait ModuleThemeTrait - default implementation of ModuleThemeInterface 51 */ 52trait ModuleThemeTrait 53{ 54 /** 55 * @return string 56 */ 57 abstract public function name(): string; 58 59 /** 60 * @return string 61 */ 62 abstract public function title(): string; 63 64 /** 65 * A sentence describing what this module does. 66 * 67 * @return string 68 */ 69 public function description(): string 70 { 71 return I18N::translate('Theme') . ' — ' . $this->title(); 72 } 73 74 /** 75 * Generate the facts, for display in charts. 76 * 77 * @param Individual $individual 78 * 79 * @return string 80 */ 81 public function individualBoxFacts(Individual $individual): string 82 { 83 $html = ''; 84 85 $opt_tags = preg_split('/\W/', $individual->tree()->getPreference('CHART_BOX_TAGS'), 0, PREG_SPLIT_NO_EMPTY); 86 // Show BIRT or equivalent event 87 foreach (Gedcom::BIRTH_EVENTS as $birttag) { 88 if (!in_array($birttag, $opt_tags, true)) { 89 $event = $individual->facts([$birttag])->first(); 90 if ($event instanceof Fact) { 91 $html .= $event->summary(); 92 break; 93 } 94 } 95 } 96 // Show optional events (before death) 97 foreach ($opt_tags as $key => $tag) { 98 if (!in_array($tag, Gedcom::DEATH_EVENTS, true)) { 99 $event = $individual->facts([$tag])->first(); 100 if ($event instanceof Fact) { 101 $html .= $event->summary(); 102 unset($opt_tags[$key]); 103 } 104 } 105 } 106 // Show DEAT or equivalent event 107 foreach (Gedcom::DEATH_EVENTS as $deattag) { 108 $event = $individual->facts([$deattag])->first(); 109 if ($event instanceof Fact) { 110 $html .= $event->summary(); 111 if (in_array($deattag, $opt_tags, true)) { 112 unset($opt_tags[array_search($deattag, $opt_tags, true)]); 113 } 114 break; 115 } 116 } 117 // Show remaining optional events (after death) 118 foreach ($opt_tags as $tag) { 119 $event = $individual->facts([$tag])->first(); 120 if ($event instanceof Fact) { 121 $html .= $event->summary(); 122 } 123 } 124 125 return $html; 126 } 127 128 /** 129 * Links, to show in chart boxes; 130 * 131 * @param Individual $individual 132 * 133 * @return Menu[] 134 */ 135 public function individualBoxMenu(Individual $individual): array 136 { 137 return array_merge( 138 $this->individualBoxMenuCharts($individual), 139 $this->individualBoxMenuFamilyLinks($individual) 140 ); 141 } 142 143 /** 144 * Chart links, to show in chart boxes; 145 * 146 * @param Individual $individual 147 * 148 * @return Menu[] 149 */ 150 public function individualBoxMenuCharts(Individual $individual): array 151 { 152 $menus = []; 153 foreach (app(ModuleService::class)->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::strcasecmp($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 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 /** @var ServerRequestInterface $request */ 204 $request = app(ServerRequestInterface::class); 205 206 $route = $request->getAttribute('route'); 207 assert($route instanceof Route); 208 209 if (Auth::check() && $route->name === UserPage::class) { 210 return new Menu(I18N::translate('Customize this page'), route(UserPageEdit::class, ['tree' => $tree->name()]), 'menu-change-blocks'); 211 } 212 213 if (Auth::isManager($tree) && $route->name === TreePage::class) { 214 return new Menu(I18N::translate('Customize this page'), route(TreePageEdit::class, ['tree' => $tree->name()]), 'menu-change-blocks'); 215 } 216 217 return null; 218 } 219 220 /** 221 * Generate a menu item for the control panel. 222 * 223 * @param Tree $tree 224 * 225 * @return Menu|null 226 */ 227 public function menuControlPanel(Tree $tree): ?Menu 228 { 229 if (Auth::isAdmin()) { 230 return new Menu(I18N::translate('Control panel'), route(ControlPanel::class), 'menu-admin'); 231 } 232 233 if (Auth::isManager($tree)) { 234 return new Menu(I18N::translate('Control panel'), route('manage-trees', ['tree' => $tree->name()]), 'menu-admin'); 235 } 236 237 return null; 238 } 239 240 /** 241 * A menu to show a list of available languages. 242 * 243 * @return Menu|null 244 */ 245 public function menuLanguages(): ?Menu 246 { 247 $menu = new Menu(I18N::translate('Language'), '#', 'menu-language'); 248 249 foreach (I18N::activeLocales() as $active_locale) { 250 $language_tag = $active_locale->languageTag(); 251 $class = 'menu-language-' . $language_tag . (I18N::languageTag() === $language_tag ? ' active' : ''); 252 $menu->addSubmenu(new Menu($active_locale->endonym(), '#', $class, [ 253 'data-post-url' => route(SelectLanguage::class, ['language' => $language_tag]), 254 ])); 255 } 256 257 if (count($menu->getSubmenus()) > 1) { 258 return $menu; 259 } 260 261 return null; 262 } 263 264 /** 265 * A login menu option (or null if we are already logged in). 266 * 267 * @return Menu|null 268 */ 269 public function menuLogin(): ?Menu 270 { 271 if (Auth::check()) { 272 return null; 273 } 274 275 $request = app(ServerRequestInterface::class); 276 277 // Return to this page after login... 278 $redirect = $request->getQueryParams()['url'] ?? (string) $request->getUri(); 279 280 $tree = $request->getAttribute('tree'); 281 $route = $request->getAttribute('route'); 282 assert($route instanceof Route); 283 284 // ...but switch from the tree-page to the user-page 285 if ($route->name === TreePage::class) { 286 $redirect = route(UserPage::class, ['tree' => $tree instanceof Tree ? $tree->name() : null]); 287 } 288 289 // Stay on the same tree page 290 $url = route(LoginPage::class, ['tree' => $tree instanceof Tree ? $tree->name() : null, 'url' => $redirect]); 291 292 return new Menu(I18N::translate('Sign in'), $url, 'menu-login', ['rel' => 'nofollow']); 293 } 294 295 /** 296 * A logout menu option (or null if we are already logged out). 297 * 298 * @return Menu|null 299 */ 300 public function menuLogout(): ?Menu 301 { 302 if (Auth::check()) { 303 $parameters = [ 304 'data-post-url' => route(Logout::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); 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 = Individual::getInstance($tree->getUserPreference(Auth::user(), User::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::id()) { 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 $gedcomid = $tree->getUserPreference(Auth::user(), User::PREF_TREE_ACCOUNT_XREF); 394 395 $pedigree_chart = app(ModuleService::class)->findByComponent(ModuleChartInterface::class, $tree, Auth::user()) 396 ->filter(static function (ModuleInterface $module): bool { 397 return $module instanceof PedigreeChartModule; 398 }); 399 400 if ($gedcomid !== '' && $pedigree_chart instanceof PedigreeChartModule) { 401 return new Menu( 402 I18N::translate('My pedigree'), 403 route('pedigree', [ 404 'xref' => $gedcomid, 405 'tree' => $tree->name(), 406 ]), 407 'menu-mypedigree' 408 ); 409 } 410 411 return null; 412 } 413 414 /** 415 * Create a pending changes menu. 416 * 417 * @param Tree|null $tree 418 * 419 * @return Menu|null 420 */ 421 public function menuPendingChanges(?Tree $tree): ?Menu 422 { 423 if ($tree instanceof Tree && $tree->hasPendingEdit() && Auth::isModerator($tree)) { 424 $url = route(PendingChanges::class, [ 425 'tree' => $tree->name(), 426 'url' => (string) app(ServerRequestInterface::class)->getUri(), 427 ]); 428 429 return new Menu(I18N::translate('Pending changes'), $url, 'menu-pending'); 430 } 431 432 return null; 433 } 434 435 /** 436 * Themes menu. 437 * 438 * @return Menu|null 439 */ 440 public function menuThemes(): ?Menu 441 { 442 $themes = app(ModuleService::class)->findByInterface(ModuleThemeInterface::class, false, true); 443 444 $current_theme = app(ModuleThemeInterface::class); 445 446 if ($themes->count() > 1) { 447 $submenus = $themes->map(static function (ModuleThemeInterface $theme) use ($current_theme): Menu { 448 $active = $theme->name() === $current_theme->name(); 449 $class = 'menu-theme-' . $theme->name() . ($active ? ' active' : ''); 450 451 return new Menu($theme->title(), '#', $class, [ 452 'data-post-url' => route(SelectTheme::class, ['theme' => $theme->name()]), 453 ]); 454 }); 455 456 return new Menu(I18N::translate('Theme'), '#', 'menu-theme', [], $submenus->all()); 457 } 458 459 return null; 460 } 461 462 /** 463 * Misecellaneous dimensions, fonts, styles, etc. 464 * 465 * @param string $parameter_name 466 * 467 * @return string|int|float 468 */ 469 public function parameter($parameter_name) 470 { 471 return ''; 472 } 473 474 /** 475 * Generate a list of items for the main menu. 476 * 477 * @param Tree|null $tree 478 * 479 * @return Menu[] 480 */ 481 public function genealogyMenu(?Tree $tree): array 482 { 483 if ($tree === null) { 484 return []; 485 } 486 487 return app(ModuleService::class)->findByComponent(ModuleMenuInterface::class, $tree, Auth::user()) 488 ->map(static function (ModuleMenuInterface $menu) use ($tree): ?Menu { 489 return $menu->getMenu($tree); 490 }) 491 ->filter() 492 ->all(); 493 } 494 495 /** 496 * Create the genealogy menu. 497 * 498 * @param Menu[] $menus 499 * 500 * @return string 501 */ 502 public function genealogyMenuContent(array $menus): string 503 { 504 return implode('', array_map(static function (Menu $menu): string { 505 return $menu->bootstrap4(); 506 }, $menus)); 507 } 508 509 /** 510 * Generate a list of items for the user menu. 511 * 512 * @param Tree|null $tree 513 * 514 * @return Menu[] 515 */ 516 public function userMenu(?Tree $tree): array 517 { 518 return array_filter([ 519 $this->menuPendingChanges($tree), 520 $this->menuMyPages($tree), 521 $this->menuThemes(), 522 $this->menuLanguages(), 523 $this->menuLogin(), 524 $this->menuLogout(), 525 ]); 526 } 527 528 /** 529 * A list of CSS files to include for this page. 530 * 531 * @return string[] 532 */ 533 public function stylesheets(): array 534 { 535 return []; 536 } 537} 538