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